From 378d929a006de8d833682e2a1bab39f9f680ce14 Mon Sep 17 00:00:00 2001 From: Simon Ebner Date: Fri, 28 Mar 2014 15:08:05 +0100 Subject: [PATCH] Initial commit --- ch.psi.imagej.cbf/.classpath | 36 + ch.psi.imagej.cbf/.gitignore | 1 + ch.psi.imagej.cbf/.project | 23 + .../org.eclipse.core.resources.prefs | 3 + .../.settings/org.eclipse.jdt.core.prefs | 5 + .../.settings/org.eclipse.m2e.core.prefs | 4 + ch.psi.imagej.cbf/Readme.md | 6 + ch.psi.imagej.cbf/doc/developer.txt | 23 + ch.psi.imagej.cbf/pom.xml | 48 + .../src/main/java/imcacat/jcbf/Base64.java | 100 + .../java/imcacat/jcbf/ByteArrayCbfData.java | 86 + .../java/imcacat/jcbf/CbfCompression.java | 52 + .../src/main/java/imcacat/jcbf/CbfData.java | 47 + .../java/imcacat/jcbf/CbfElementType.java | 78 + .../java/imcacat/jcbf/CbfImageReadParam.java | 56 + .../java/imcacat/jcbf/CbfImageReader.java | 1386 +++++++++++++ .../java/imcacat/jcbf/CbfImageReaderSpi.java | 75 + .../java/imcacat/jcbf/CbfImageWriteParam.java | 56 + .../java/imcacat/jcbf/CbfImageWriter.java | 773 ++++++++ .../java/imcacat/jcbf/CbfImageWriterSpi.java | 63 + .../main/java/imcacat/jcbf/CbfMetadata.java | 760 ++++++++ .../java/imcacat/jcbf/CbfMetadataFormat.java | 39 + .../imcacat/jcbf/ImageInputStreamCbfData.java | 88 + .../java/imcacat/jcbf/ImageJCbfReader.java | 224 +++ .../src/main/java/imcacat/jcbf/Version.java | 28 + .../src/main/resources/.gitignore | 0 .../test/java/imcacat/jcbf/Base64Test.java | 87 + .../java/imcacat/jcbf/CbfCompressionTest.java | 34 + .../java/imcacat/jcbf/CbfElementTypeTest.java | 34 + .../imcacat/jcbf/CbfImageReaderSpiTest.java | 74 + .../java/imcacat/jcbf/CbfImageReaderTest.java | 953 +++++++++ .../imcacat/jcbf/CbfImageWriterSpiTest.java | 39 + .../java/imcacat/jcbf/CbfImageWriterTest.java | 1731 +++++++++++++++++ .../java/imcacat/jcbf/CbfMetadataTest.java | 83 + .../src/test/java/imcacat/jcbf/TestUtils.java | 474 +++++ .../src/test/resources/.gitignore | 0 36 files changed, 7569 insertions(+) create mode 100644 ch.psi.imagej.cbf/.classpath create mode 100644 ch.psi.imagej.cbf/.gitignore create mode 100644 ch.psi.imagej.cbf/.project create mode 100644 ch.psi.imagej.cbf/.settings/org.eclipse.core.resources.prefs create mode 100644 ch.psi.imagej.cbf/.settings/org.eclipse.jdt.core.prefs create mode 100644 ch.psi.imagej.cbf/.settings/org.eclipse.m2e.core.prefs create mode 100644 ch.psi.imagej.cbf/Readme.md create mode 100644 ch.psi.imagej.cbf/doc/developer.txt create mode 100644 ch.psi.imagej.cbf/pom.xml create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/Base64.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ByteArrayCbfData.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfCompression.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfData.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfElementType.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReadParam.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReader.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReaderSpi.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriteParam.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriter.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriterSpi.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfMetadata.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfMetadataFormat.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ImageInputStreamCbfData.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ImageJCbfReader.java create mode 100644 ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/Version.java create mode 100644 ch.psi.imagej.cbf/src/main/resources/.gitignore create mode 100644 ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/Base64Test.java create mode 100644 ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfCompressionTest.java create mode 100644 ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfElementTypeTest.java create mode 100644 ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageReaderSpiTest.java create mode 100644 ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageReaderTest.java create mode 100644 ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageWriterSpiTest.java create mode 100644 ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageWriterTest.java create mode 100644 ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfMetadataTest.java create mode 100644 ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/TestUtils.java create mode 100644 ch.psi.imagej.cbf/src/test/resources/.gitignore diff --git a/ch.psi.imagej.cbf/.classpath b/ch.psi.imagej.cbf/.classpath new file mode 100644 index 0000000..953de0b --- /dev/null +++ b/ch.psi.imagej.cbf/.classpath @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ch.psi.imagej.cbf/.gitignore b/ch.psi.imagej.cbf/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/ch.psi.imagej.cbf/.gitignore @@ -0,0 +1 @@ +/target diff --git a/ch.psi.imagej.cbf/.project b/ch.psi.imagej.cbf/.project new file mode 100644 index 0000000..6be7461 --- /dev/null +++ b/ch.psi.imagej.cbf/.project @@ -0,0 +1,23 @@ + + + ch.psi.imagej.cbf + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/ch.psi.imagej.cbf/.settings/org.eclipse.core.resources.prefs b/ch.psi.imagej.cbf/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..5b781ec --- /dev/null +++ b/ch.psi.imagej.cbf/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 diff --git a/ch.psi.imagej.cbf/.settings/org.eclipse.jdt.core.prefs b/ch.psi.imagej.cbf/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..ec4300d --- /dev/null +++ b/ch.psi.imagej.cbf/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/ch.psi.imagej.cbf/.settings/org.eclipse.m2e.core.prefs b/ch.psi.imagej.cbf/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/ch.psi.imagej.cbf/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/ch.psi.imagej.cbf/Readme.md b/ch.psi.imagej.cbf/Readme.md new file mode 100644 index 0000000..f9d213a --- /dev/null +++ b/ch.psi.imagej.cbf/Readme.md @@ -0,0 +1,6 @@ +Jcbf ([J]ava [C]rystallographic [B]inary [F]ile) is a pure Java library for +reading and writing minimal CBF (miniCBF) image files using the MIME +"mini-header" and the "none" or "byte_offset" compression algorithm, as +produced by DECTRIS PILATUS detectors. + +Written in March 2011 by J. Lewis Muir (IMCA-CAT). diff --git a/ch.psi.imagej.cbf/doc/developer.txt b/ch.psi.imagej.cbf/doc/developer.txt new file mode 100644 index 0000000..861d390 --- /dev/null +++ b/ch.psi.imagej.cbf/doc/developer.txt @@ -0,0 +1,23 @@ +Code +---- +* Follow style of existing code. +* Max code line length: 100 characters. +* Max comment line length: 76 characters. + +Retrieve Library Dependencies +----------------------------- +* Required for compiling from source. Requires network connection. Only + needs to be performed once, or after running the distclean target: + $ ant fetch + +Release +------- +* Change version property in build.properties. +* Compile to update source with new version ID: + $ ant compile +* Check in any changes to version control system. +* Run test cases: + $ ant clean test +* Make distribution (dist-bin for binary distribution): + $ ant clean dist-src +* Tag version in version control system. diff --git a/ch.psi.imagej.cbf/pom.xml b/ch.psi.imagej.cbf/pom.xml new file mode 100644 index 0000000..eacc1c9 --- /dev/null +++ b/ch.psi.imagej.cbf/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + ch.psi.imagej + ch.psi.imagej.cbf + 0.0.1 + + + + gov.nih.imagej + imagej + 1.46 + provided + + + junit + junit + 4.11 + test + + + + + + + maven-compiler-plugin + 2.3.2 + + UTF-8 + 1.7 + 1.7 + + + + maven-assembly-plugin + 2.4 + + CBF_Viewer-${pom.version} + false + + + jar-with-dependencies + + + + + + \ No newline at end of file diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/Base64.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/Base64.java new file mode 100644 index 0000000..0ecdacd --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/Base64.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.nio.ByteBuffer; + +/** + * I am a base 64 codec implementing the "base64" encoding in RFC 4648. I do + * not add line feeds to the encoded data. + */ +public class Base64 { + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789" + + "+/"; + private static final char PAD = '='; + + public static String encode(byte[] value) { + StringBuffer result = new StringBuffer(encodedSize(value)); + for (int i = 0; i < value.length; i += 3) { + int octet1 = value[i] & 0xFF; + int octet2 = (i + 1 < value.length) ? value[i + 1] & 0xFF : 0; + int octet3 = (i + 2 < value.length) ? value[i + 2] & 0xFF : 0; + int group = (octet1 << 16) | (octet2 << 8) | octet3; + int sextetBitmask = 0x3F; + int sextet1 = (group >> 18) & sextetBitmask; + int sextet2 = (group >> 12) & sextetBitmask; + int sextet3 = (group >> 6) & sextetBitmask; + int sextet4 = group & sextetBitmask; + result.append(encode(sextet1)); + result.append(encode(sextet2)); + result.append((i + 1 < value.length) ? encode(sextet3) : PAD); + result.append((i + 2 < value.length) ? encode(sextet4) : PAD); + } + return result.toString(); + } + + private static int encodedSize(byte[] value) { + if ((value.length % 3) == 0) return value.length * 4 / 3; + if ((value.length % 3) == 1) return (value.length + 2) * 4 / 3; + return (value.length + 1) * 4 / 3; + } + + private static char encode(int sextet) { + return ALPHABET.charAt(sextet); + } + + public static byte[] decode(String encoded) { + ByteBuffer result = ByteBuffer.wrap(new byte[decodedSize(encoded)]); + for (int i = 0; i < encoded.length(); i += 4) { + boolean atEnd = i + 4 >= encoded.length(); + int sextet1 = decode(encoded.charAt(i)); + int sextet2 = decode(encoded.charAt(i + 1)); + int sextet3 = (atEnd && encoded.charAt(i + 2) == PAD) ? 0 : decode(encoded.charAt(i + 2)); + int sextet4 = (atEnd && encoded.charAt(i + 3) == PAD) ? 0 : decode(encoded.charAt(i + 3)); + int group = (sextet1 << 18) | (sextet2 << 12) | (sextet3 << 6) | sextet4; + int octetBitmask = 0xFF; + byte octet1 = (byte)((group >> 16) & octetBitmask); + byte octet2 = (byte)((group >> 8) & octetBitmask); + byte octet3 = (byte)(group & octetBitmask); + result.put(octet1); + if (!atEnd || encoded.charAt(i + 2) != PAD) result.put(octet2); + if (!atEnd || encoded.charAt(i + 3) != PAD) result.put(octet3); + } + return result.array(); + } + + private static int decodedSize(String encoded) { + if ((encoded.length() % 4) != 0) { + throw new IllegalArgumentException("Encoded string has invalid length"); + } + String pad = String.valueOf(PAD); + if (encoded.endsWith(pad + pad)) return (encoded.length() * 3 / 4) - 2; + if (encoded.endsWith(pad)) return (encoded.length() * 3 / 4) - 1; + return encoded.length() * 3 / 4; + } + + private static int decode(char encodedSextet) { + if (encodedSextet >= 'A' && encodedSextet <= 'Z') return encodedSextet - 'A'; + if (encodedSextet >= 'a' && encodedSextet <= 'z') return encodedSextet - 'a' + 26; + if (encodedSextet >= '0' && encodedSextet <= '9') return encodedSextet - '0' + 52; + if (encodedSextet == '+') return 62; + if (encodedSextet == '/') return 63; + throw new IllegalArgumentException("Letter not in alphabet: '" + encodedSextet + "'"); + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ByteArrayCbfData.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ByteArrayCbfData.java new file mode 100644 index 0000000..3f76033 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ByteArrayCbfData.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * I am a CbfData backed by an array of bytes. + */ +public class ByteArrayCbfData implements CbfData { + ByteBuffer data; + + public ByteArrayCbfData(byte[] data) { + this.data = ByteBuffer.wrap(data); + } + + @Override + public void order(ByteOrder bo) { + data.order(bo); + } + + @Override + public byte getByte() { + return data.get(); + } + + @Override + public short getUbyte() { + return (short)(data.get() & 0xFFFF); + } + + @Override + public short getShort() { + return data.getShort(); + } + + @Override + public int getUshort() { + return data.getShort() & 0xFFFFFFFF; + } + + @Override + public int getInt() { + return data.getInt(); + } + + @Override + public long getUint() { + return data.getInt() & 0xFFFFFFFFL; + } + + @Override + public long getLong() { + return data.getLong(); + } + + @Override + public float getFloat() { + return data.getFloat(); + } + + @Override + public double getDouble() { + return data.getDouble(); + } + + @Override + public void skipBytes(int count) { + data.position(data.position() + count); + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfCompression.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfCompression.java new file mode 100644 index 0000000..09a9ac2 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfCompression.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.text.ParseException; + +/** + * I am the type of compression used to compress the data of a CBF image. + */ +public enum CbfCompression { +/* @formatter:off */ + NONE( "x-CBF_NONE"), + BYTE_OFFSET("x-CBF_BYTE_OFFSET"); +/* @formatter:on */ + + private String contentTypeConversions; + + CbfCompression(String contentTypeConversions) { + this.contentTypeConversions = contentTypeConversions; + } + + public boolean compatibleWith(CbfElementType type) { + if (equals(NONE)) return true; + return !CbfElementType.SIGNED_32_BIT_REAL_IEEE.equals(type) + && !CbfElementType.SIGNED_64_BIT_REAL_IEEE.equals(type); + } + + public String toContentTypeConversions() { + return contentTypeConversions; + } + + public static CbfCompression parse(String contentTypeConversions) throws ParseException { + String trimmed = contentTypeConversions.trim(); + for (CbfCompression each : values()) + if (each.toContentTypeConversions().equals(trimmed)) return each; + throw new ParseException("Unsupported conversions: " + trimmed, 0); + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfData.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfData.java new file mode 100644 index 0000000..19b9637 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfData.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.io.IOException; +import java.nio.ByteOrder; + +/** + * I am an interface for reading CBF image data. + */ +public interface CbfData { + void order(ByteOrder bo); + + byte getByte() throws IOException; + + short getUbyte() throws IOException; + + short getShort() throws IOException; + + int getUshort() throws IOException; + + int getInt() throws IOException; + + long getUint() throws IOException; + + long getLong() throws IOException; + + float getFloat() throws IOException; + + double getDouble() throws IOException; + + void skipBytes(int count) throws IOException; +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfElementType.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfElementType.java new file mode 100644 index 0000000..545e8d5 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfElementType.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.awt.image.DataBuffer; +import java.text.ParseException; + +/** + * I am the type of an element in the data of a CBF image. + */ +public enum CbfElementType { +/* @formatter:off */ + UNSIGNED_8_BIT_INTEGER( "unsigned 8-bit integer", false), + SIGNED_8_BIT_INTEGER( "signed 8-bit integer", true), + UNSIGNED_16_BIT_INTEGER("unsigned 16-bit integer", false), + SIGNED_16_BIT_INTEGER( "signed 16-bit integer", true), + UNSIGNED_32_BIT_INTEGER("unsigned 32-bit integer", false), + SIGNED_32_BIT_INTEGER( "signed 32-bit integer", true), + SIGNED_32_BIT_REAL_IEEE("signed 32-bit real IEEE", true), + SIGNED_64_BIT_REAL_IEEE("signed 64-bit real IEEE", true); +/* @formatter:on */ + + private String xBinaryElementType; + private boolean signed; + + CbfElementType(String xBinaryElementType, boolean signed) { + this.xBinaryElementType = xBinaryElementType; + this.signed = signed; + } + + public String toXBinaryElementType() { + return xBinaryElementType; + } + + public boolean isSigned() { + return signed; + } + + public static CbfElementType parse(String xBinaryElementType) throws ParseException { + String trimmed = xBinaryElementType.trim(); + for (CbfElementType each : values()) + if (each.toXBinaryElementType().equals(trimmed)) return each; + throw new ParseException("Unsupported type: " + trimmed, 0); + } + + public static CbfElementType fromDataBufferType(int dataBufferType) { + switch (dataBufferType) { + case DataBuffer.TYPE_BYTE: + return UNSIGNED_8_BIT_INTEGER; + case DataBuffer.TYPE_USHORT: + return UNSIGNED_16_BIT_INTEGER; + case DataBuffer.TYPE_SHORT: + return SIGNED_16_BIT_INTEGER; + case DataBuffer.TYPE_INT: + return SIGNED_32_BIT_INTEGER; + case DataBuffer.TYPE_FLOAT: + return SIGNED_32_BIT_REAL_IEEE; + case DataBuffer.TYPE_DOUBLE: + return SIGNED_64_BIT_REAL_IEEE; + default: + return SIGNED_32_BIT_INTEGER; + } + } +} \ No newline at end of file diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReadParam.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReadParam.java new file mode 100644 index 0000000..4f3b953 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReadParam.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import javax.imageio.ImageReadParam; + +/** + * I am an ImageReadParam for CbfImageReader. + */ +public class CbfImageReadParam extends ImageReadParam { + private boolean minimizeMemoryUse = false; + + @Override + public boolean canSetSourceRenderSize() { + return false; + } + + /** + * Indicates whether memory use during an image read should be minimized. + * + * @return true if memory use should be minimized; + * false otherwise + * + * @see #setMinimizeMemoryUse(boolean) + */ + public boolean shouldMinimizeMemoryUse() { + return minimizeMemoryUse; + } + + /** + * Sets whether memory use during an image read should be minimized. + * Minimizing memory use will likely increase processor use and execution + * time. However, if dealing with very large images or a system with very + * little memory, this may be appropriate. The default is + * false. + * + * @param minimizeMemoryUse whether memory use should be memorized + */ + public void setMinimizeMemoryUse(boolean minimizeMemoryUse) { + this.minimizeMemoryUse = minimizeMemoryUse; + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReader.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReader.java new file mode 100644 index 0000000..58664e8 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReader.java @@ -0,0 +1,1386 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.awt.Rectangle; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.imageio.IIOException; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + +/** + * I read a minimal CBF (miniCBF) image as produced by DECTRIS PILATUS + * detectors. I only support images using the MIME mini-header and the + * "none" or "byte_offset" compression algorithms. I do not support the + * "unsigned 1-bit integer" and "signed 32-bit complex IEEE" data element + * types. + */ +public class CbfImageReader extends ImageReader { + private CbfMetadata metadata; + + public CbfImageReader(ImageReaderSpi originatingProvider) { + super(originatingProvider); + } + + @Override + public int getNumImages(boolean allowSearch) throws IOException { + return 1; + } + + @Override + public int getWidth(int imageIndex) throws IOException { + checkIndex(imageIndex); + return getMetadata().getDataWidth(); + } + + @Override + public int getHeight(int imageIndex) throws IOException { + checkIndex(imageIndex); + return getMetadata().getDataHeight(); + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + checkIndex(imageIndex); + List result = new ArrayList(); + CbfElementType elementType = getMetadata().getDataElementType(); + switch (elementType) { + case UNSIGNED_8_BIT_INTEGER: + result.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY)); + break; + case SIGNED_8_BIT_INTEGER: + /* + * WORKAROUND: ImageTypeSpecifier.createGrayscale throws an + * IllegalArgumentException if specify 8 bits and + * DataBuffer.TYPE_BYTE. The documentation for the createGrayscale + * method leads me to believe it should work, but it doesn't. So, + * using 16 bits and DataBuffer.TYPE_SHORT instead. + */ + result.add(ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, true)); + break; + case UNSIGNED_16_BIT_INTEGER: + result.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY)); + break; + case SIGNED_16_BIT_INTEGER: + result.add(ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, true)); + break; + case UNSIGNED_32_BIT_INTEGER: + result.add(ImageTypeSpecifier.createInterleaved( + ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] { 0 }, DataBuffer.TYPE_FLOAT, + false, false)); + result.add(ImageTypeSpecifier.createInterleaved( + ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] { 0 }, DataBuffer.TYPE_INT, false, + false)); + break; + case SIGNED_32_BIT_INTEGER: + result.add(ImageTypeSpecifier.createInterleaved( + ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] { 0 }, DataBuffer.TYPE_INT, false, + false)); + result.add(ImageTypeSpecifier.createInterleaved( + ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] { 0 }, DataBuffer.TYPE_FLOAT, + false, false)); + break; + case SIGNED_32_BIT_REAL_IEEE: + result.add(ImageTypeSpecifier.createInterleaved( + ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] { 0 }, DataBuffer.TYPE_FLOAT, + false, false)); + break; + case SIGNED_64_BIT_REAL_IEEE: + result.add(ImageTypeSpecifier.createInterleaved( + ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] { 0 }, DataBuffer.TYPE_DOUBLE, + false, false)); + break; + default: + throw new RuntimeException("Bug; element type: " + elementType); + } + return result.iterator(); + } + + @Override + public IIOMetadata getStreamMetadata() throws IOException { + return null; + } + + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + checkIndex(imageIndex); + return getMetadata(); + } + + @Override + public ImageReadParam getDefaultReadParam() { + return new CbfImageReadParam(); + } + + /** + * Reads a CBF image. + *

+ * The destination type for unsigned 32-bit integer source data is + * float by default, but int is supported too. + * When the destination type is float, some less significant + * digits can be lost. When the destination type is int, + * values larger than Integer.MAX_VALUE will be capped at + * Integer.MAX_VALUE and a warning will be sent to registered + * IIOReadWarningListener listeners. + */ + /* + * Optimized for speed. Reading the image data all at once into memory + * results in a 25 times speed-up. Avoiding method invocations also speeds + * things up which is why this method is quite long and has duplicate code + * in the byte_offset decompression implementation. + */ + @Override + public BufferedImage read(int imageIndex, ImageReadParam irparam) throws IOException { + checkIndex(imageIndex); + clearAbortRequest(); + processImageStarted(0); + + ImageReadParam param = (irparam == null) ? getDefaultReadParam() : irparam; + boolean minimizeMemoryUse = false; + if (param instanceof CbfImageReadParam) { + minimizeMemoryUse = ((CbfImageReadParam)param).shouldMinimizeMemoryUse(); + } + int srcWidth = getWidth(0); + int srcHeight = getHeight(0); + + BufferedImage dstImage = getDestination(param, getImageTypes(0), srcWidth, srcHeight); + Rectangle srcRegion = new Rectangle(); + Rectangle dstRegion = new Rectangle(); + computeRegions(param, srcWidth, srcHeight, dstImage, srcRegion, dstRegion); + int srcRegionMaxX = srcRegion.x + srcRegion.width - 1; + int srcRegionMaxY = srcRegion.y + srcRegion.height - 1; + int dstRegionMaxX = dstRegion.x + dstRegion.width - 1; + + int srcXSubsamp = param.getSourceXSubsampling(); + int srcYSubsamp = param.getSourceYSubsampling(); + + int dstDataType = dstImage.getSampleModel().getDataType(); + checkReadParamBandSettings(param, 1, dstImage.getSampleModel().getNumBands()); + int[] dstBands = param.getDestinationBands(); + int dstBand = (dstBands == null) ? 0 : dstBands[0]; + WritableRaster dstRaster = dstImage.getRaster(); + + CbfMetadata mdata = getMetadata(); + ImageInputStream stream = getNonNullInput(); + long streamStartOfData = stream.getStreamPosition(); + stream.mark(); + + CbfData data; + byte[] dataArray = null; + int dataSize = mdata.getDataSize(); + + MessageDigest md5Digest = null; + byte[] expectedMd5 = mdata.getDataMd5(); + byte[] actualMd5 = null; + if (expectedMd5 != null) { + try { + md5Digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + processWarningOccurred("Failed to find MD5 algorithm for data MD5 digest calculation"); + } + } + + if (minimizeMemoryUse || dataSize == 0) { + data = new ImageInputStreamCbfData(stream); + } else { + dataArray = new byte[dataSize]; + stream.readFully(dataArray); + data = new ByteArrayCbfData(dataArray); + } + + if (dataArray != null && md5Digest != null) { + md5Digest.update(dataArray, 0, dataArray.length); + actualMd5 = md5Digest.digest(); + if (!MessageDigest.isEqual(expectedMd5, actualMd5)) { + throw new IIOException("MIME header MD5 does not match data"); + } + } + + data.order(mdata.getDataElementByteOrder()); + + boolean readEntireImage = srcRegion.width == srcWidth && srcRegion.height == srcHeight; + int progressRowPeriod = Math.max(1, (srcRegionMaxY + 1) / 100); + CbfElementType elementType = mdata.getDataElementType(); + boolean unsigned = !elementType.isSigned(); + + if (mdata.getDataCompression().equals(CbfCompression.NONE)) { + long elementCount = 0; + int dstY = dstRegion.y; + int srcYSubsampCount = 0; + for (int srcY = 0; srcY < srcHeight; srcY++) { + if (abortRequested()) { + processReadAborted(); + return dstImage; + } + if (srcY % progressRowPeriod == 0) { + processImageProgress(((float)srcY / (float)(srcRegionMaxY + 1)) * 100.0f); + } + if (srcYSubsamp != 1 && srcY > srcRegion.y && srcY <= srcRegionMaxY) { + srcYSubsampCount++; + if (srcYSubsampCount == srcYSubsamp) srcYSubsampCount = 0; + } + int dstX = dstRegion.x; + int srcXSubsampCount = 0; + for (int srcX = 0; srcX < srcWidth; srcX++) { + if (srcXSubsamp != 1 && srcX > srcRegion.y && srcX <= srcRegionMaxX) { + srcXSubsampCount++; + if (srcXSubsampCount == srcXSubsamp) srcXSubsampCount = 0; + } + boolean shouldSet = + srcXSubsampCount == 0 && srcYSubsampCount == 0 && srcRegion.contains(srcX, srcY); + if (!shouldSet && srcX > srcRegionMaxX && srcY > srcRegionMaxY) { + srcY = srcHeight; + srcX = srcWidth; + break; + } + switch (elementType) { + case UNSIGNED_8_BIT_INTEGER: + if (shouldSet) { + dstRaster.setSample(dstX, dstY, dstBand, data.getUbyte()); + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } else { + data.skipBytes(1); + } + break; + case SIGNED_8_BIT_INTEGER: + if (shouldSet) { + dstRaster.setSample(dstX, dstY, dstBand, data.getByte()); + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } else { + data.skipBytes(1); + } + break; + case UNSIGNED_16_BIT_INTEGER: + if (shouldSet) { + dstRaster.setSample(dstX, dstY, dstBand, data.getUshort()); + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } else { + data.skipBytes(2); + } + break; + case SIGNED_16_BIT_INTEGER: + if (shouldSet) { + dstRaster.setSample(dstX, dstY, dstBand, data.getShort()); + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } else { + data.skipBytes(2); + } + break; + case UNSIGNED_32_BIT_INTEGER: + if (shouldSet) { + long value = data.getUint(); + if (dstDataType == DataBuffer.TYPE_FLOAT) { + dstRaster.setSample(dstX, dstY, dstBand, (float)value); + } else { + if (value <= Integer.MAX_VALUE) { + dstRaster.setSample(dstX, dstY, dstBand, (int)value); + } else { + dstRaster.setSample(dstX, dstY, dstBand, Integer.MAX_VALUE); + processWarningOccurred("Decoded pixel value " + value + " capped to " + + Integer.MAX_VALUE + " to fit in signed 32-bit integer range"); + } + } + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } else { + data.skipBytes(4); + } + break; + case SIGNED_32_BIT_INTEGER: + if (shouldSet) { + dstRaster.setSample(dstX, dstY, dstBand, data.getInt()); + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } else { + data.skipBytes(4); + } + break; + case SIGNED_32_BIT_REAL_IEEE: + if (shouldSet) { + dstRaster.setSample(dstX, dstY, dstBand, data.getFloat()); + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } else { + data.skipBytes(4); + } + break; + case SIGNED_64_BIT_REAL_IEEE: + if (shouldSet) { + dstRaster.setSample(dstX, dstY, dstBand, data.getDouble()); + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } else { + data.skipBytes(8); + } + break; + default: + throw new RuntimeException("Bug; element type: " + elementType); + } + elementCount++; + } + } + if (readEntireImage && elementCount != mdata.getDataNumElements()) { + throw new IIOException("Expected " + mdata.getDataNumElements() + + " element count, but was " + elementCount); + } + } else if (mdata.getDataCompression().equals(CbfCompression.BYTE_OFFSET)) { + long delta = 0; + long value = 0; + long elementCount = 0; + int dstY = dstRegion.y; + int srcYSubsampCount = 0; + for (int srcY = 0; srcY < srcHeight; srcY++) { + if (abortRequested()) { + processReadAborted(); + return dstImage; + } + if (srcY % progressRowPeriod == 0) { + processImageProgress(((float)srcY / (float)(srcRegionMaxY + 1)) * 100.0f); + } + if (srcYSubsamp != 1 && srcY > srcRegion.y && srcY <= srcRegionMaxY) { + srcYSubsampCount++; + if (srcYSubsampCount == srcYSubsamp) srcYSubsampCount = 0; + } + int dstX = dstRegion.x; + int srcXSubsampCount = 0; + for (int srcX = 0; srcX < srcWidth; srcX++) { + if (srcXSubsamp != 1 && srcX > srcRegion.x && srcX <= srcRegionMaxX) { + srcXSubsampCount++; + if (srcXSubsampCount == srcXSubsamp) srcXSubsampCount = 0; + } + boolean shouldSet = + srcXSubsampCount == 0 && srcYSubsampCount == 0 && srcRegion.contains(srcX, srcY); + if (!shouldSet && srcX > srcRegionMaxX && srcY > srcRegionMaxY) { + srcX = srcWidth; + srcY = srcHeight; + break; + } + delta = data.getByte(); + if (delta != Byte.MIN_VALUE) { + value += delta; + if (unsigned) value &= 0xFFFFFFFFL; + if (shouldSet) { + if (CbfElementType.UNSIGNED_32_BIT_INTEGER.equals(elementType)) { + if (dstDataType == DataBuffer.TYPE_FLOAT) { + dstRaster.setSample(dstX, dstY, dstBand, (float)value); + } else { + if (value <= Integer.MAX_VALUE) { + dstRaster.setSample(dstX, dstY, dstBand, (int)value); + } else { + dstRaster.setSample(dstX, dstY, dstBand, Integer.MAX_VALUE); + processWarningOccurred("Decoded pixel value " + value + " capped to " + + Integer.MAX_VALUE + " to fit in signed 32-bit integer range"); + } + } + } else { + dstRaster.setSample(dstX, dstY, dstBand, (int)value); + } + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } + elementCount++; + continue; + } + delta = data.getShort(); + if (delta != Short.MIN_VALUE) { + value += delta; + if (unsigned) value &= 0xFFFFFFFFL; + if (shouldSet) { + if (CbfElementType.UNSIGNED_32_BIT_INTEGER.equals(elementType)) { + if (dstDataType == DataBuffer.TYPE_FLOAT) { + dstRaster.setSample(dstX, dstY, dstBand, (float)value); + } else { + if (value <= Integer.MAX_VALUE) { + dstRaster.setSample(dstX, dstY, dstBand, (int)value); + } else { + dstRaster.setSample(dstX, dstY, dstBand, Integer.MAX_VALUE); + processWarningOccurred("Decoded pixel value " + value + " capped to " + + Integer.MAX_VALUE + " to fit in signed 32-bit integer range"); + } + } + } else { + dstRaster.setSample(dstX, dstY, dstBand, (int)value); + } + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } + elementCount++; + continue; + } + delta = data.getInt(); + if (delta != Integer.MIN_VALUE) { + value += delta; + if (unsigned) value &= 0xFFFFFFFFL; + if (shouldSet) { + if (CbfElementType.UNSIGNED_32_BIT_INTEGER.equals(elementType)) { + if (dstDataType == DataBuffer.TYPE_FLOAT) { + dstRaster.setSample(dstX, dstY, dstBand, (float)value); + } else { + if (value <= Integer.MAX_VALUE) { + dstRaster.setSample(dstX, dstY, dstBand, (int)value); + } else { + dstRaster.setSample(dstX, dstY, dstBand, Integer.MAX_VALUE); + processWarningOccurred("Decoded pixel value " + value + " capped to " + + Integer.MAX_VALUE + " to fit in signed 32-bit integer range"); + } + } + } else { + dstRaster.setSample(dstX, dstY, dstBand, (int)value); + } + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } + elementCount++; + continue; + } + delta = data.getLong(); + value += delta; + if (unsigned) value &= 0xFFFFFFFFL; + if (shouldSet) { + if (CbfElementType.UNSIGNED_32_BIT_INTEGER.equals(elementType)) { + if (dstDataType == DataBuffer.TYPE_FLOAT) { + dstRaster.setSample(dstX, dstY, dstBand, (float)value); + } else { + if (value <= Integer.MAX_VALUE) { + dstRaster.setSample(dstX, dstY, dstBand, (int)value); + } else { + dstRaster.setSample(dstX, dstY, dstBand, Integer.MAX_VALUE); + processWarningOccurred("Decoded pixel value " + value + " capped to " + + Integer.MAX_VALUE + " to fit in signed 32-bit integer range"); + } + } + } else { + dstRaster.setSample(dstX, dstY, dstBand, (int)value); + } + dstX++; + if (dstX > dstRegionMaxX) dstY++; + } + elementCount++; + } + } + if (readEntireImage && elementCount != mdata.getDataNumElements()) { + throw new IIOException("Expected " + mdata.getDataNumElements() + + " element count, but was " + elementCount); + } + } else { + throw new RuntimeException("Bug; compression: " + mdata.getDataCompression()); + } + + if (abortRequested()) { + processReadAborted(); + return dstImage; + } + + if (readEntireImage && md5Digest != null && dataArray == null) { + long streamEndOfData = stream.getStreamPosition(); + if (streamEndOfData - streamStartOfData <= Integer.MAX_VALUE) { + dataSize = (int)(streamEndOfData - streamStartOfData); + if (minimizeMemoryUse) { + for (int i = 0; i < dataSize; i++) + md5Digest.update(stream.readByte()); + } else { + dataArray = new byte[dataSize]; + stream.reset(); + stream.readFully(dataArray); + md5Digest.update(dataArray, 0, dataArray.length); + } + actualMd5 = md5Digest.digest(); + if (!MessageDigest.isEqual(expectedMd5, actualMd5)) { + throw new IIOException("MIME header MD5 does not match data"); + } + } + } + + processImageComplete(); + + return dstImage; + } + + private void checkIndex(int imageIndex) { + if (imageIndex != 0) throw new IndexOutOfBoundsException("imageIndex != 0"); + } + + private ImageInputStream getNonNullInput() { + Object input = getInput(); + if (input == null) throw new IllegalStateException("No input stream"); + return (ImageInputStream)input; + } + + private CbfMetadata getMetadata() throws IIOException { + if (metadata == null) readMetadata(); + return metadata; + } + + private void readMetadata() throws IIOException { + ImageInputStream stream = getNonNullInput(); + CbfMetadata mdata = new CbfMetadata(); + readMetadataIdentifier(stream, mdata); + readMetadataDataBlockName(stream, mdata); + readMetadataHeader(stream, mdata); + readMetadataMimeMiniHeader(stream, mdata); + metadata = mdata; + } + + private void readMetadataIdentifier(ImageInputStream stream, CbfMetadata mdata) + throws IIOException { + String identifier; + try { + identifier = stream.readLine(); + } catch (IOException e) { + throw new IIOException("Error reading identifier", e); + } + if (!identifier.startsWith("###CBF: VERSION")) throw new IIOException("Not a CBF file"); + mdata.setIdentifier(identifier); + } + + private void readMetadataDataBlockName(ImageInputStream stream, CbfMetadata mdata) + throws IIOException { + try { + String line = readNonemptyNoncommentLine(stream); + if (!line.startsWith("data_")) throw new IIOException("Expected data block declaration"); + mdata.setDataBlockName(line.substring("data_".length())); + } catch (IOException e) { + throw new IIOException("Error reading data block name", e); + } + } + + private void readMetadataHeader(ImageInputStream stream, CbfMetadata mdata) + throws IIOException { + try { + String line = readNonemptyNoncommentLine(stream); + if (!line.startsWith("_array_data.header_convention ")) { + throw new IIOException("Expected _array_data.header_convention tag"); + } + String convention = trimQuotes(line.substring("_array_data.header_convention ".length())); + if (!convention.startsWith("SLS_1.")) { + throw new IIOException("Unrecognized header convention: " + convention); + } + mdata.setHeaderConvention(convention); + if (!convention.equals("SLS_1.0")) { + processWarningOccurred("Unrecognized header convention: " + convention); + } + line = readNonemptyNoncommentLine(stream); + if (!line.equals("_array_data.header_contents")) { + throw new IIOException("Expected _array_data.header_contents tag"); + } + line = readNonemptyNoncommentLine(stream); + if (!line.equals(";")) { + throw new IIOException("Expected ';' line after _array_data.header_contents"); + } + + Pattern datePattern = Pattern.compile("^# \\d{4}"); + Pattern sensorPattern = Pattern.compile("^# .+ sensor, *thickness "); + for (;;) { + line = readNonemptyLine(stream); + if (line.equals(";")) break; + if (line.startsWith("# Detector:")) { + String value = line.substring("# Detector:".length()); + mdata.setHeaderDetector(parseHeaderDetector(value)); + } else if (datePattern.matcher(line).lookingAt()) { + String value = line.substring("# ".length()); + mdata.setHeaderDate(parseHeaderDate(value)); + } else if (line.startsWith("# Pixel_size ")) { + String value = line.substring("# Pixel_size ".length()); + mdata.setHeaderPixelSize(parseHeaderPixelSize(value)); + } else if (sensorPattern.matcher(line).lookingAt()) { + String value = line.substring("# ".length()); + Object[] typeAndThickness = parseHeaderSensor(value); + mdata.setHeaderSensorType((String)typeAndThickness[0]); + mdata.setHeaderSensorThickness((Float)typeAndThickness[1]); + } else if (line.startsWith("# Exposure_time ")) { + String value = line.substring("# Exposure_time ".length()); + mdata.setHeaderExposureTime(parseHeaderExposureTime(value)); + } else if (line.startsWith("# Exposure_period ")) { + String value = line.substring("# Exposure_period ".length()); + mdata.setHeaderExposurePeriod(parseHeaderExposurePeriod(value)); + } else if (line.startsWith("# Tau =")) { + String value = line.substring("# Tau =".length()); + mdata.setHeaderTau(parseHeaderTau(value)); + } else if (line.startsWith("# Count_cutoff ")) { + String value = line.substring("# Count_cutoff ".length()); + mdata.setHeaderCountCutoff(parseHeaderCountCutoff(value)); + } else if (line.startsWith("# Threshold_setting ")) { + String value = line.substring("# Threshold_setting ".length()); + mdata.setHeaderThreshold(parseHeaderThreshold(value)); + } else if (line.startsWith("# Gain_setting ")) { + String value = line.substring("# Gain_setting ".length()); + Object[] typeAndVrf = parseHeaderGain(value); + mdata.setHeaderGainType((String)typeAndVrf[0]); + mdata.setHeaderGainVrf((Float)typeAndVrf[1]); + } else if (line.startsWith("# N_excluded_pixels =")) { + String value = line.substring("# N_excluded_pixels =".length()); + mdata.setHeaderNumExcludedPixels(parseHeaderNumExcludedPixels(value)); + } else if (line.startsWith("# Excluded_pixels:")) { + String value = line.substring("# Excluded_pixels:".length()); + mdata.setHeaderExcludedPixels(parseHeaderExcludedPixels(value)); + } else if (line.startsWith("# Flat_field:")) { + String value = line.substring("# Flat_field:".length()); + mdata.setHeaderFlatField(parseHeaderFlatField(value)); + } else if (line.startsWith("# Trim_directory:")) { + String value = line.substring("# Trim_directory:".length()); + mdata.setHeaderTrim(parseHeaderTrim(value)); + } else if (line.startsWith("# Wavelength ")) { + String value = line.substring("# Wavelength ".length()); + mdata.setHeaderWavelength(parseHeaderWavelength(value)); + } else if (line.startsWith("# Energy_range ")) { + String value = line.substring("# Energy_range ".length()); + mdata.setHeaderEnergyRange(parseHeaderEnergyRange(value)); + } else if (line.startsWith("# Detector_distance ")) { + String value = line.substring("# Detector_distance ".length()); + mdata.setHeaderDetectorDistance(parseHeaderDetectorDistance(value)); + } else if (line.startsWith("# Detector_Voffset ")) { + String value = line.substring("# Detector_Voffset ".length()); + mdata.setHeaderDetectorVOffset(parseHeaderDetectorVOffset(value)); + } else if (line.startsWith("# Beam_xy ")) { + String value = line.substring("# Beam_xy ".length()); + mdata.setHeaderBeamXY(parseHeaderBeamXY(value)); + } else if (line.startsWith("# Flux ")) { + String value = line.substring("# Flux ".length()); + mdata.setHeaderFlux(parseHeaderFlux(value)); + } else if (line.startsWith("# Filter_transmission ")) { + String value = line.substring("# Filter_transmission ".length()); + mdata.setHeaderFilterTransmission(parseHeaderFilterTransmission(value)); + } else if (line.startsWith("# Start_angle ")) { + String value = line.substring("# Start_angle ".length()); + mdata.setHeaderStartAngle(parseHeaderStartAngle(value)); + } else if (line.startsWith("# Angle_increment ")) { + String value = line.substring("# Angle_increment ".length()); + mdata.setHeaderAngleIncrement(parseHeaderAngleIncrement(value)); + } else if (line.startsWith("# Detector_2theta ")) { + String value = line.substring("# Detector_2theta ".length()); + mdata.setHeaderDetector2Theta(parseHeaderDetector2Theta(value)); + } else if (line.startsWith("# Polarization ")) { + String value = line.substring("# Polarization ".length()); + mdata.setHeaderPolarization(parseHeaderPolarization(value)); + } else if (line.startsWith("# Alpha ")) { + String value = line.substring("# Alpha ".length()); + mdata.setHeaderAlpha(parseHeaderAlpha(value)); + } else if (line.startsWith("# Kappa ")) { + String value = line.substring("# Kappa ".length()); + mdata.setHeaderKappa(parseHeaderKappa(value)); + } else if (line.startsWith("# Phi ")) { + String value = line.substring("# Phi ".length()); + mdata.setHeaderPhi(parseHeaderPhi(value)); + } else if (line.startsWith("# Chi ")) { + String value = line.substring("# Chi ".length()); + mdata.setHeaderChi(parseHeaderChi(value)); + } else if (line.startsWith("# Oscillation_axis ")) { + String value = line.substring("# Oscillation_axis ".length()); + mdata.setHeaderOscillationAxis(parseHeaderOscillationAxis(value)); + } else if (line.startsWith("# N_oscillations ")) { + String value = line.substring("# N_oscillations ".length()); + mdata.setHeaderNumOscillations(parseHeaderNumOscillations(value)); + } else { + mdata.addHeaderUnrecognizedLine(line); + processWarningOccurred("Unrecognized header line: " + line); + } + } + } catch (IOException e) { + throw new IIOException("Error reading header", e); + } + } + + private String parseHeaderDetector(String value) { + return value.trim(); + } + + private Date parseHeaderDate(String value) throws IIOException { + String dateText = value.trim(); + String[] formatTexts = { "yyyy-MMM-dd'T'HH:mm:ss.SSS", "yyyy/MMM/dd HH:mm:ss.SSS" }; + for (String each : formatTexts) { + DateFormat format = new SimpleDateFormat(each, Locale.US); + ParsePosition position = new ParsePosition(0); + Date result = format.parse(dateText, position); + if (result != null && position.getIndex() >= dateText.length()) return result; + } + throw new IIOException("Error parsing header date: " + dateText); + } + + private float[] parseHeaderPixelSize(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +m +x +(.+) +m"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header pixel size: " + trimmedValue); + } + float[] result = new float[2]; + for (int i = 0; i < result.length; i++) { + String text = matcher.group(i + 1); + try { + result[i] = Float.parseFloat(text); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header pixel size dimension: " + text, e); + } + } + return result; + } + + private Object[] parseHeaderSensor(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +sensor, *thickness +(.+) +m"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header sensor: " + trimmedValue); + } + String type = matcher.group(1).trim(); + String thicknessText = matcher.group(2).trim(); + Float thickness; + try { + thickness = Float.parseFloat(thicknessText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header sensor thickness: " + thicknessText, e); + } + return new Object[] { type, thickness }; + } + + private Float parseHeaderExposureTime(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +s"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header exposure time: " + trimmedValue); + } + String timeText = matcher.group(1).trim(); + try { + return Float.parseFloat(timeText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header exposure time: " + timeText, e); + } + } + + private Float parseHeaderExposurePeriod(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +s"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header exposure period: " + trimmedValue); + } + String periodText = matcher.group(1).trim(); + try { + return Float.parseFloat(periodText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header exposure period: " + periodText, e); + } + } + + private Float parseHeaderTau(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +s"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header tau: " + trimmedValue); + } + String tauText = matcher.group(1).trim(); + try { + return Float.parseFloat(tauText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header tau: " + tauText, e); + } + } + + private Integer parseHeaderCountCutoff(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +counts"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header count cutoff: " + trimmedValue); + } + String countText = matcher.group(1).trim(); + try { + return Integer.parseInt(countText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header count cutoff: " + countText, e); + } + } + + private Float parseHeaderThreshold(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +eV"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header threshold: " + trimmedValue); + } + String thresholdText = matcher.group(1).trim(); + try { + return Float.parseFloat(thresholdText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header threshold: " + thresholdText, e); + } + } + + private Object[] parseHeaderGain(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) \\(vrf = (.+)\\)"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header gain: " + trimmedValue); + } + String type = matcher.group(1).trim(); + if (type.equals("not implemented")) type = null; + String vrfText = matcher.group(2).trim(); + Float vrf; + try { + vrf = Float.parseFloat(vrfText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header gain vrf: " + vrfText, e); + } + return new Object[] { type, vrf }; + } + + private Integer parseHeaderNumExcludedPixels(String value) throws IIOException { + String trimmedValue = value.trim(); + try { + return Integer.parseInt(trimmedValue); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header number of excluded pixels: " + trimmedValue, e); + } + } + + private String parseHeaderExcludedPixels(String value) throws IIOException { + String trimmedValue = value.trim(); + return (trimmedValue.length() == 0 || trimmedValue.equals("(nil)")) ? null : trimmedValue; + } + + private String parseHeaderFlatField(String value) throws IIOException { + String trimmedValue = value.trim(); + return (trimmedValue.length() == 0 || trimmedValue.equals("(nil)")) ? null : trimmedValue; + } + + private String parseHeaderTrim(String value) throws IIOException { + String trimmedValue = value.trim(); + return (trimmedValue.length() == 0 || trimmedValue.equals("(nil)")) ? null : trimmedValue; + } + + private Float parseHeaderWavelength(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +A"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header wavelength: " + trimmedValue); + } + String wavelengthText = matcher.group(1).trim(); + try { + return Float.parseFloat(wavelengthText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header wavelength: " + wavelengthText, e); + } + } + + private float[] parseHeaderEnergyRange(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "\\((.+), *(.+)\\) +eV"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header energy range: " + trimmedValue); + } + float[] result = new float[2]; + for (int i = 0; i < result.length; i++) { + String text = matcher.group(i + 1); + try { + result[i] = Float.parseFloat(text); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header energy range: " + text, e); + } + } + return result; + } + + private Float parseHeaderDetectorDistance(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +m"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header detector distance: " + trimmedValue); + } + String distanceText = matcher.group(1).trim(); + try { + return Float.parseFloat(distanceText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header detector distance: " + distanceText, e); + } + } + + private Float parseHeaderDetectorVOffset(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +m"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header detector vertical offset: " + trimmedValue); + } + String offsetText = matcher.group(1).trim(); + try { + return Float.parseFloat(offsetText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header detector vertical offset: " + offsetText, e); + } + } + + private float[] parseHeaderBeamXY(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "\\((.+), *(.+)\\) +pixels"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header beam XY: " + trimmedValue); + } + float[] result = new float[2]; + for (int i = 0; i < result.length; i++) { + String text = matcher.group(i + 1); + try { + result[i] = Float.parseFloat(text); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header beam XY: " + text, e); + } + } + return result; + } + + private Float parseHeaderFlux(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +ph/s"; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header flux: " + trimmedValue); + } + String fluxText = matcher.group(1).trim(); + try { + return Float.parseFloat(fluxText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header flux: " + fluxText, e); + } + } + + private Float parseHeaderFilterTransmission(String value) throws IIOException { + String trimmedValue = value.trim(); + try { + return Float.parseFloat(trimmedValue); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header filter transmission: " + trimmedValue, e); + } + } + + private Float parseHeaderStartAngle(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +deg\\."; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header start angle: " + trimmedValue); + } + String angleText = matcher.group(1).trim(); + try { + return Float.parseFloat(angleText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header start angle: " + angleText, e); + } + } + + private Float parseHeaderAngleIncrement(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +deg\\."; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header angle increment: " + trimmedValue); + } + String incrementText = matcher.group(1).trim(); + try { + return Float.parseFloat(incrementText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header angle increment: " + incrementText, e); + } + } + + private Float parseHeaderDetector2Theta(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +deg\\."; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header detector 2theta: " + trimmedValue); + } + String angleText = matcher.group(1).trim(); + try { + return Float.parseFloat(angleText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header detector 2theta: " + angleText, e); + } + } + + private Float parseHeaderPolarization(String value) throws IIOException { + String trimmedValue = value.trim(); + try { + return Float.parseFloat(trimmedValue); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header polarization: " + trimmedValue, e); + } + } + + private Float parseHeaderAlpha(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +deg\\."; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header alpha: " + trimmedValue); + } + String angleText = matcher.group(1).trim(); + try { + return Float.parseFloat(angleText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header alpha: " + angleText, e); + } + } + + private Float parseHeaderKappa(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +deg\\."; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header kappa: " + trimmedValue); + } + String angleText = matcher.group(1).trim(); + try { + return Float.parseFloat(angleText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header kappa: " + angleText, e); + } + } + + private Float parseHeaderPhi(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +deg\\."; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header phi: " + trimmedValue); + } + String angleText = matcher.group(1).trim(); + try { + return Float.parseFloat(angleText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header phi: " + angleText, e); + } + } + + private Float parseHeaderChi(String value) throws IIOException { + String trimmedValue = value.trim(); + String regex = "(.+) +deg\\."; + Matcher matcher = Pattern.compile(regex).matcher(trimmedValue); + boolean matches = matcher.matches(); + if (!matches) { + throw new IIOException("Error parsing header chi: " + trimmedValue); + } + String angleText = matcher.group(1).trim(); + try { + return Float.parseFloat(angleText); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header chi: " + angleText, e); + } + } + + private String parseHeaderOscillationAxis(String value) { + return value.trim(); + } + + private Integer parseHeaderNumOscillations(String value) throws IIOException { + String trimmedValue = value.trim(); + try { + return Integer.parseInt(trimmedValue); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing header number of oscillations: " + trimmedValue, e); + } + } + + private void readMetadataMimeMiniHeader(ImageInputStream stream, CbfMetadata mdata) + throws IIOException { + try { + String line = readNonemptyNoncommentLine(stream); + if (!line.equals("_array_data.data")) { + throw new IIOException("Expected _array_data.data tag"); + } + line = readNonemptyNoncommentLine(stream); + if (!line.equals(";")) { + throw new IIOException("Expected ';' line after _array_data.data"); + } + line = readNonemptyNoncommentLine(stream); + if (!line.equals("--CIF-BINARY-FORMAT-SECTION--")) { + throw new IIOException("Expected --CIF-BINARY-FORMAT-SECTION--"); + } + + String nextLine = stream.readLine(); + for (;;) { + line = nextLine; + if (line.length() == 0) break; + nextLine = stream.readLine(); + if (nextLine.startsWith(" ") || nextLine.startsWith("\t")) { + nextLine = line + nextLine; + } else if (line.startsWith("Content-Type:")) { + String value = line.substring("Content-Type:".length()); + Object[] contentType = parseMimeMiniHeaderContentType(value); + CbfCompression comp = (CbfCompression)contentType[1]; + CbfElementType type = mdata.getDataElementType(); + if (type != null && !comp.compatibleWith(type)) { + throw new IIOException(comp + " compression incompatible with element type " + type); + } + mdata.setDataCompression(comp); + } else if (line.startsWith("Content-Transfer-Encoding:")) { + String value = line.substring("Content-Transfer-Encoding:".length()); + parseMimeMiniHeaderContentTransferEncoding(value); + } else if (line.startsWith("X-Binary-Size:")) { + String value = line.substring("X-Binary-Size:".length()); + mdata.setDataSize(parseMimeMiniHeaderBinarySize(value)); + } else if (line.startsWith("X-Binary-ID:")) { + String value = line.substring("X-Binary-ID:".length()); + mdata.setDataId(parseMimeMiniHeaderBinaryId(value)); + } else if (line.startsWith("X-Binary-Element-Type:")) { + String value = line.substring("X-Binary-Element-Type:".length()); + CbfElementType type = parseMimeMiniHeaderBinaryElementType(value); + CbfCompression comp = mdata.getDataCompression(); + if (comp != null && !comp.compatibleWith(type)) { + throw new IIOException(comp + " compression incompatible with element type " + type); + } + mdata.setDataElementType(type); + } else if (line.startsWith("X-Binary-Element-Byte-Order:")) { + String value = line.substring("X-Binary-Element-Byte-Order:".length()); + mdata.setDataElementByteOrder(parseMimeMiniHeaderBinaryElementByteOrder(value)); + } else if (line.startsWith("Content-MD5:")) { + String value = line.substring("Content-MD5:".length()); + mdata.setDataMd5(parseMimeMiniHeaderContentMd5(value)); + } else if (line.startsWith("X-Binary-Number-of-Elements:")) { + String value = line.substring("X-Binary-Number-of-Elements:".length()); + mdata.setDataNumElements(parseMimeMiniHeaderBinaryNumberOfElements(value)); + } else if (line.startsWith("X-Binary-Size-Fastest-Dimension:")) { + String value = line.substring("X-Binary-Size-Fastest-Dimension:".length()); + mdata.setDataWidth(parseMimeMiniHeaderBinarySizeFastestDimension(value)); + } else if (line.startsWith("X-Binary-Size-Second-Dimension:")) { + String value = line.substring("X-Binary-Size-Second-Dimension:".length()); + mdata.setDataHeight(parseMimeMiniHeaderBinarySizeSecondDimension(value)); + } else if (line.startsWith("X-Binary-Size-Padding:")) { + String value = line.substring("X-Binary-Size-Padding:".length()); + mdata.setDataPadding(parseMimeMiniHeaderBinarySizePadding(value)); + } else { + processWarningOccurred("Unrecognized MIME mini-header line: " + line); + } + } + + byte[] octets = { 0x0C, 0x1A, 0x04, (byte)0xD5 }; + for (int i = 0; i < octets.length; i++) { + byte octet = stream.readByte(); + if (octet != octets[i]) { + throw new IIOException(String.format( + "Expected %#x in octet series before binary data, but was %#x", octets[i], octet)); + } + } + } catch (IOException e) { + throw new IIOException("Error reading MIME mini-header", e); + } + } + + private Object[] parseMimeMiniHeaderContentType(String value) throws IIOException { + int semicolonIndex = value.indexOf(';'); + if (semicolonIndex < 0) semicolonIndex = value.length(); + String mediaType = value.substring(0, semicolonIndex).trim(); + if (!mediaType.equals("application/octet-stream")) { + throw new IIOException("Unrecognized content type media type: " + mediaType); + } + int conversionsIndex = value.indexOf("conversions=", semicolonIndex + 1); + if (conversionsIndex < 0) { + throw new IIOException("Expected content type conversions; default x-CBF_PACKED unsupported"); + } + String conversions = value.substring(conversionsIndex + "conversions=".length()).trim(); + if (conversions.startsWith("\"")) conversions = conversions.substring(1); + int doubleQuoteIndex = conversions.indexOf('"'); + if (doubleQuoteIndex >= 0) conversions = conversions.substring(0, doubleQuoteIndex); + try { + return new Object[] { mediaType, CbfCompression.parse(conversions) }; + } catch (ParseException e) { + throw new IIOException("Unsupported content type conversions: " + conversions, e); + } + } + + private void parseMimeMiniHeaderContentTransferEncoding(String value) throws IIOException { + String encoding = value.trim(); + if (encoding.equals("BINARY")) return; + throw new IIOException("Unexpected encoding: " + encoding); + } + + private int parseMimeMiniHeaderBinarySize(String value) throws IIOException { + String size = value.trim(); + try { + return Integer.parseInt(size); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing binary size: " + size); + } + } + + private int parseMimeMiniHeaderBinaryId(String value) throws IIOException { + String id = value.trim(); + try { + return Integer.parseInt(id); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing binary ID: " + id); + } + } + + private CbfElementType parseMimeMiniHeaderBinaryElementType(String value) throws IIOException { + String type = trimQuotes(value.trim()); + try { + return CbfElementType.parse(type); + } catch (ParseException e) { + throw new IIOException("Unsupported binary element type: " + type, e); + } + } + + private ByteOrder parseMimeMiniHeaderBinaryElementByteOrder(String value) throws IIOException { + String order = value.trim(); + if (order.equals("LITTLE_ENDIAN")) { + return ByteOrder.LITTLE_ENDIAN; + } else if (order.equals("BIG_ENDIAN")) { + return ByteOrder.BIG_ENDIAN; + } else { + throw new IIOException("Error parsing binary element byte order: " + order); + } + } + + private byte[] parseMimeMiniHeaderContentMd5(String value) throws IIOException { + String md5 = value.trim(); + try { + return Base64.decode(md5); + } catch (IllegalArgumentException e) { + throw new IIOException("Error decoding MD5 digest: " + md5, e); + } + } + + private int parseMimeMiniHeaderBinaryNumberOfElements(String value) throws IIOException { + String count = value.trim(); + try { + return Integer.parseInt(count); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing binary number of elements: " + count, e); + } + } + + private int parseMimeMiniHeaderBinarySizeFastestDimension(String value) throws IIOException { + String size = value.trim(); + try { + return Integer.parseInt(size); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing binary size fastest dimension: " + size, e); + } + } + + private int parseMimeMiniHeaderBinarySizeSecondDimension(String value) throws IIOException { + String size = value.trim(); + try { + return Integer.parseInt(size); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing binary size second dimension: " + size, e); + } + } + + private int parseMimeMiniHeaderBinarySizePadding(String value) throws IIOException { + String padding = value.trim(); + try { + return Integer.parseInt(padding); + } catch (NumberFormatException e) { + throw new IIOException("Error parsing binary size padding: " + padding, e); + } + } + + private String readNonemptyLine(ImageInputStream stream) throws IOException { + String line; + for (;;) { + line = stream.readLine().trim(); + if (line.length() != 0) return line; + } + } + + private String readNonemptyNoncommentLine(ImageInputStream stream) throws IOException { + String line; + for (;;) { + line = stream.readLine().trim(); + if (line.length() != 0 && !line.startsWith("#")) return line; + } + } + + private String trimQuotes(String s) { + if (s.length() == 0) return s; + int start = 0; + int end = s.length(); + for (int i = start; i < end; i++) { + if (s.charAt(i) != '\'' && s.charAt(i) != '"') { + start = i; + break; + } + } + for (int i = end - 1; i > start; i--) { + if (s.charAt(i) != '\'' && s.charAt(i) != '"') { + end = i + 1; + break; + } + } + return s.substring(start, end); + } + + @Override + public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { + super.setInput(input, seekForwardOnly, ignoreMetadata); + metadata = null; + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReaderSpi.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReaderSpi.java new file mode 100644 index 0000000..f0b4cff --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageReaderSpi.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; + +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + +/** + * I am a reader SPI for CBF images. + */ +public class CbfImageReaderSpi extends ImageReaderSpi { + private static final String VENDOR_NAME = "IMCA-CAT"; + private static final String VERSION = Version.ID; + private static final String[] FORMAT_NAMES = { "cbf", "CBF" }; + private static final String[] FORMAT_SUFFIXES = { "cbf" }; + private static final String[] MIME_TYPES = { "image/x-cbf" }; + private static final String READER_CLASS_NAME = "imcacat.jcbf.CbfImageReader"; + private static final String[] WRITER_SPI_CLASS_NAMES = { "imcacat.jcbf.CbfImageWriterSpi" }; + private static final String METADATA_FORMAT_NAME = "imcacat.jcbf.CbfMetadataFormat_1.0"; + private static final String METADATA_FORMAT_CLASS_NAME = "imcacat.jcbf.CbfMetadataFormat"; + + public CbfImageReaderSpi() { + super(VENDOR_NAME, VERSION, FORMAT_NAMES, FORMAT_SUFFIXES, MIME_TYPES, READER_CLASS_NAME, + STANDARD_INPUT_TYPE, WRITER_SPI_CLASS_NAMES, false, null, null, null, null, false, + METADATA_FORMAT_NAME, METADATA_FORMAT_CLASS_NAME, null, null); + } + + @Override + public boolean canDecodeInput(Object source) throws IOException { + if (!(source instanceof ImageInputStream)) return false; + ImageInputStream stream = (ImageInputStream)source; + String magicNumber = "###CBF: VERSION"; + byte[] expected = magicNumber.getBytes("US-ASCII"); + byte[] actual = new byte[expected.length]; + stream.mark(); + try { + stream.readFully(actual); + return Arrays.equals(expected, actual); + } catch (EOFException e) { + return false; + } finally { + stream.reset(); + } + } + + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + return new CbfImageReader(this); + } + + @Override + public String getDescription(Locale locale) { + return "CBF image reader"; + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriteParam.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriteParam.java new file mode 100644 index 0000000..cd03894 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriteParam.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import javax.imageio.ImageWriteParam; + +/** + * I am an ImageWriteParam for CbfImageWriter. + */ +public class CbfImageWriteParam extends ImageWriteParam { + public static final String COMPRESSION_NONE = "None"; + public static final String COMPRESSION_BYTE_OFFSET = "Byte_offset"; + + public CbfImageWriteParam() { + super(); + } + + @Override + public boolean canWriteTiles() { + return false; + } + + @Override + public boolean canOffsetTiles() { + return false; + } + + @Override + public boolean canWriteProgressive() { + return false; + } + + @Override + public boolean canWriteCompressed() { + return true; + } + + @Override + public String[] getCompressionTypes() { + return new String[] { COMPRESSION_NONE, COMPRESSION_BYTE_OFFSET }; + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriter.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriter.java new file mode 100644 index 0000000..ca59a92 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriter.java @@ -0,0 +1,773 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Locale; + +import javax.imageio.IIOException; +import javax.imageio.IIOImage; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.IIOByteBuffer; +import javax.imageio.stream.ImageOutputStream; + +/** + * I write a minimal CBF (miniCBF) image like that produced by DECTRIS + * PILATUS detectors. I only write images using the MIME mini-header and the + * "none" or "byte_offset" compression algorithms. I do not support the + * "unsigned 1-bit integer" and "signed 32-bit complex IEEE" data element + * types. + */ +public class CbfImageWriter extends ImageWriter { + public CbfImageWriter(ImageWriterSpi originatingProvider) { + super(originatingProvider); + } + + @Override + public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { + return null; + } + + @Override + public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { + CbfMetadata mdata = new CbfMetadata(); + + int bufferedImageType = imageType.getBufferedImageType(); + int dataBufferType = imageType.getSampleModel().getDataType(); + if (bufferedImageType == BufferedImage.TYPE_BYTE_BINARY) { + mdata.setDataElementType(CbfElementType.UNSIGNED_8_BIT_INTEGER); + } else if (bufferedImageType == BufferedImage.TYPE_BYTE_GRAY) { + mdata.setDataElementType(CbfElementType.UNSIGNED_8_BIT_INTEGER); + } else if (bufferedImageType == BufferedImage.TYPE_USHORT_GRAY) { + mdata.setDataElementType(CbfElementType.UNSIGNED_16_BIT_INTEGER); + } else if (dataBufferType == DataBuffer.TYPE_BYTE) { + mdata.setDataElementType(CbfElementType.UNSIGNED_8_BIT_INTEGER); + } else if (dataBufferType == DataBuffer.TYPE_SHORT) { + mdata.setDataElementType(CbfElementType.SIGNED_16_BIT_INTEGER); + } else if (dataBufferType == DataBuffer.TYPE_FLOAT) { + mdata.setDataElementType(CbfElementType.SIGNED_32_BIT_REAL_IEEE); + } else if (dataBufferType == DataBuffer.TYPE_INT) { + mdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + } else if (dataBufferType == DataBuffer.TYPE_DOUBLE) { + mdata.setDataElementType(CbfElementType.SIGNED_64_BIT_REAL_IEEE); + } + + int compressionMode = param.getCompressionMode(); + if (compressionMode == ImageWriteParam.MODE_DISABLED) { + mdata.setDataCompression(CbfCompression.NONE); + } else if (compressionMode == ImageWriteParam.MODE_EXPLICIT) { + if (param.getCompressionType().equals(CbfImageWriteParam.COMPRESSION_BYTE_OFFSET)) { + mdata.setDataCompression(CbfCompression.BYTE_OFFSET); + } else if (param.getCompressionType().equals(CbfImageWriteParam.COMPRESSION_NONE)) { + mdata.setDataCompression(CbfCompression.NONE); + } + } + + return mdata; + } + + @Override + public ImageWriteParam getDefaultWriteParam() { + return new CbfImageWriteParam(); + } + + @Override + public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) { + return null; + } + + @Override + public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, + ImageWriteParam param) { + return (inData instanceof CbfMetadata) ? inData : null; + } + + @Override + public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam iwparam) + throws IOException { + clearAbortRequest(); + processImageStarted(0); + + RenderedImage srcImage = image.getRenderedImage(); + ImageWriteParam param = (iwparam == null) ? getDefaultWriteParam() : iwparam; + + CbfMetadata mdata = null; + IIOMetadata srcMetadata = image.getMetadata(); + if (srcMetadata != null) { + mdata = + (CbfMetadata)convertImageMetadata(srcMetadata, + ImageTypeSpecifier.createFromRenderedImage(srcImage), param); + } + if (mdata == null) mdata = + (CbfMetadata)getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(srcImage), + param); + + IIOByteBuffer encodedImage = encode(image, mdata, param); + if (encodedImage == null) return; + + if (abortRequested()) { + processWriteAborted(); + return; + } + + ImageOutputStream stream = getNonNullOutput(); + + stream.writeBytes(mdata.getIdentifier() + "\r\n"); + stream.writeBytes("\r\n"); + stream.writeBytes("data_" + mdata.getDataBlockName() + "\r\n"); + stream.writeBytes("\r\n"); + stream.writeBytes("_array_data.header_convention \"" + mdata.getHeaderConvention() + "\"\r\n"); + stream.writeBytes("_array_data.header_contents\r\n"); + stream.writeBytes(";\r\n"); + if (mdata.getHeaderDetector() != null) { + stream.writeBytes("# Detector: "); + stream.writeBytes(mdata.getHeaderDetector()); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderDate() != null) { + stream.writeBytes("# "); + DateFormat format = new SimpleDateFormat("yyyy-MMM-dd'T'HH:mm:ss.SSS", Locale.US); + stream.writeBytes(format.format(mdata.getHeaderDate())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderPixelSize() != null) { + stream.writeBytes("# Pixel_size "); + float[] size = mdata.getHeaderPixelSize(); + size[0] *= 1e6f; + size[1] *= 1e6f; + stream.writeBytes(String.format(Locale.US, "%.0fe-6 m x %.0fe-6 m", size[0], size[1])); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderSensorType() != null && mdata.getHeaderSensorThickness() != null) { + stream.writeBytes("# "); + stream.writeBytes(mdata.getHeaderSensorType()); + stream.writeBytes(" sensor, thickness "); + stream.writeBytes(String.format(Locale.US, "%.6f m", mdata.getHeaderSensorThickness())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderExposureTime() != null) { + stream.writeBytes("# Exposure_time "); + stream.writeBytes(String.format(Locale.US, "%.6f s", mdata.getHeaderExposureTime())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderExposurePeriod() != null) { + stream.writeBytes("# Exposure_period "); + stream.writeBytes(String.format(Locale.US, "%.6f s", mdata.getHeaderExposurePeriod())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderTau() != null) { + stream.writeBytes("# Tau = "); + stream.writeBytes(String.format(Locale.US, "%.1fe-09 s", 1e9f * mdata.getHeaderTau())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderCountCutoff() != null) { + stream.writeBytes("# Count_cutoff "); + stream.writeBytes(String.format(Locale.US, "%d counts", mdata.getHeaderCountCutoff())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderThreshold() != null) { + stream.writeBytes("# Threshold_setting "); + stream.writeBytes(String.format(Locale.US, "%.0f eV", mdata.getHeaderThreshold())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderGainVrf() != null) { + stream.writeBytes("# Gain_setting "); + String type = mdata.getHeaderGainType(); + stream.writeBytes((type == null) ? "not implemented" : type); + stream.writeBytes(String.format(Locale.US, " (vrf = %.3f)", mdata.getHeaderGainVrf())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderNumExcludedPixels() != null) { + stream.writeBytes("# N_excluded_pixels = "); + stream.writeBytes(String.format(Locale.US, "%d", mdata.getHeaderNumExcludedPixels())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderExcludedPixels() != null) { + stream.writeBytes("# Excluded_pixels: "); + stream.writeBytes(mdata.getHeaderExcludedPixels()); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderFlatField() != null) { + stream.writeBytes("# Flat_field: "); + stream.writeBytes(mdata.getHeaderFlatField()); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderTrim() != null) { + stream.writeBytes("# Trim_directory: "); + stream.writeBytes(mdata.getHeaderTrim()); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderWavelength() != null) { + stream.writeBytes("# Wavelength "); + stream.writeBytes(String.format(Locale.US, "%.4f A", mdata.getHeaderWavelength())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderEnergyRange() != null) { + stream.writeBytes("# Energy_range "); + float[] range = mdata.getHeaderEnergyRange(); + stream.writeBytes(String.format(Locale.US, "(%.0f, %.0f) eV", range[0], range[1])); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderDetectorDistance() != null) { + stream.writeBytes("# Detector_distance "); + stream.writeBytes(String.format(Locale.US, "%.5f m", mdata.getHeaderDetectorDistance())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderDetectorVOffset() != null) { + stream.writeBytes("# Detector_Voffset "); + stream.writeBytes(String.format(Locale.US, "%.5f m", mdata.getHeaderDetectorVOffset())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderBeamXY() != null) { + stream.writeBytes("# Beam_xy "); + float[] beam = mdata.getHeaderBeamXY(); + stream.writeBytes(String.format(Locale.US, "(%.2f, %.2f) pixels", beam[0], beam[1])); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderFlux() != null) { + stream.writeBytes("# Flux "); + stream.writeBytes(String.format(Locale.US, "%.4f ph/s", mdata.getHeaderFlux())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderFilterTransmission() != null) { + stream.writeBytes("# Filter_transmission "); + stream.writeBytes(String.format(Locale.US, "%.4f", mdata.getHeaderFilterTransmission())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderStartAngle() != null) { + stream.writeBytes("# Start_angle "); + stream.writeBytes(String.format(Locale.US, "%.4f deg.", mdata.getHeaderStartAngle())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderAngleIncrement() != null) { + stream.writeBytes("# Angle_increment "); + stream.writeBytes(String.format(Locale.US, "%.4f deg.", mdata.getHeaderAngleIncrement())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderDetector2Theta() != null) { + stream.writeBytes("# Detector_2theta "); + stream.writeBytes(String.format(Locale.US, "%.4f deg.", mdata.getHeaderDetector2Theta())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderPolarization() != null) { + stream.writeBytes("# Polarization "); + stream.writeBytes(String.format(Locale.US, "%.3f", mdata.getHeaderPolarization())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderAlpha() != null) { + stream.writeBytes("# Alpha "); + stream.writeBytes(String.format(Locale.US, "%.4f deg.", mdata.getHeaderAlpha())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderKappa() != null) { + stream.writeBytes("# Kappa "); + stream.writeBytes(String.format(Locale.US, "%.4f deg.", mdata.getHeaderKappa())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderPhi() != null) { + stream.writeBytes("# Phi "); + stream.writeBytes(String.format(Locale.US, "%.4f deg.", mdata.getHeaderPhi())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderChi() != null) { + stream.writeBytes("# Chi "); + stream.writeBytes(String.format(Locale.US, "%.4f deg.", mdata.getHeaderChi())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderOscillationAxis() != null) { + stream.writeBytes("# Oscillation_axis "); + stream.writeBytes(mdata.getHeaderOscillationAxis()); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderNumOscillations() != null) { + stream.writeBytes("# N_oscillations "); + stream.writeBytes(String.format(Locale.US, "%d", mdata.getHeaderNumOscillations())); + stream.writeBytes("\r\n"); + } + if (mdata.getHeaderComment() != null) { + stream.writeBytes("# Comment: "); + stream.writeBytes(mdata.getHeaderComment()); + stream.writeBytes("\r\n"); + } + for (String each : mdata.getHeaderUnrecognizedLines()) { + stream.writeBytes(each); + stream.writeBytes("\r\n"); + } + stream.writeBytes(";\r\n"); + stream.writeBytes("\r\n"); + + stream.writeBytes("_array_data.data\r\n"); + stream.writeBytes(";\r\n"); + + stream.writeBytes("--CIF-BINARY-FORMAT-SECTION--\r\n"); + stream.writeBytes("Content-Type: application/octet-stream;\r\n"); + stream.writeBytes(" conversions=\""); + stream.writeBytes(mdata.getDataCompression().toContentTypeConversions()); + stream.writeBytes("\"\r\n"); + stream.writeBytes("Content-Transfer-Encoding: BINARY\r\n"); + stream.writeBytes("X-Binary-Size: "); + stream.writeBytes(String.format(Locale.US, "%d", mdata.getDataSize())); + stream.writeBytes("\r\n"); + stream.writeBytes("X-Binary-ID: "); + stream.writeBytes(String.format(Locale.US, "%d", mdata.getDataId())); + stream.writeBytes("\r\n"); + stream.writeBytes("X-Binary-Element-Type: \""); + stream.writeBytes(mdata.getDataElementType().toXBinaryElementType()); + stream.writeBytes("\"\r\n"); + stream.writeBytes("X-Binary-Element-Byte-Order: "); + stream.writeBytes(ByteOrder.LITTLE_ENDIAN.equals(mdata.getDataElementByteOrder()) + ? "LITTLE_ENDIAN" : "BIG_ENDIAN"); + stream.writeBytes("\r\n"); + stream.writeBytes("Content-MD5: "); + stream.writeBytes(Base64.encode(mdata.getDataMd5())); + stream.writeBytes("\r\n"); + stream.writeBytes("X-Binary-Number-of-Elements: "); + stream.writeBytes(String.format(Locale.US, "%d", mdata.getDataNumElements())); + stream.writeBytes("\r\n"); + stream.writeBytes("X-Binary-Size-Fastest-Dimension: "); + stream.writeBytes(String.format(Locale.US, "%d", mdata.getDataWidth())); + stream.writeBytes("\r\n"); + stream.writeBytes("X-Binary-Size-Second-Dimension: "); + stream.writeBytes(String.format(Locale.US, "%d", mdata.getDataHeight())); + stream.writeBytes("\r\n"); + stream.writeBytes("X-Binary-Size-Padding: "); + stream.writeBytes(String.format(Locale.US, "%d", mdata.getDataPadding())); + stream.writeBytes("\r\n"); + stream.writeBytes("\r\n"); + stream.write(new byte[] { 0x0C, 0x1A, 0x04, (byte)0xD5 }); + stream.write(encodedImage.getData(), 0, encodedImage.getLength()); + stream.write(new byte[mdata.getDataPadding()]); + stream.writeBytes("\r\n"); + stream.writeBytes("--CIF-BINARY-FORMAT-SECTION----\r\n"); + stream.writeBytes(";\r\n"); + stream.writeBytes("\r\n"); + stream.flush(); + + processImageComplete(); + } + + /* + * Ignores IIOMetadata of specified IIOImage; instead uses specified + * CbfMetadata. Specified ImageWriteParam takes precedence over specified + * CbfMetadata. Specified CbfMetadata will be modified as needed to match + * the encoding. + */ + private IIOByteBuffer encode(IIOImage iimage, CbfMetadata mdata, ImageWriteParam param) + throws IIOException { + RenderedImage rimage = iimage.getRenderedImage(); + + int dataBufferType = rimage.getSampleModel().getDataType(); + CbfElementType elementType = mdata.getDataElementType(); + if (elementType == null) { + elementType = CbfElementType.fromDataBufferType(dataBufferType); + mdata.setDataElementType(elementType); + } + + CbfCompression compression = mdata.getDataCompression(); + if (compression == null) compression = CbfCompression.BYTE_OFFSET; + if (param.getCompressionMode() == ImageWriteParam.MODE_DEFAULT) { + compression = CbfCompression.BYTE_OFFSET; + } else if (param.getCompressionMode() == ImageWriteParam.MODE_DISABLED) { + compression = CbfCompression.NONE; + } else if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) { + if (CbfImageWriteParam.COMPRESSION_BYTE_OFFSET.equals(param.getCompressionType())) { + compression = CbfCompression.BYTE_OFFSET; + } else { + compression = CbfCompression.NONE; + } + } + if (!compression.compatibleWith(elementType)) compression = CbfCompression.NONE; + + if (CbfCompression.BYTE_OFFSET.equals(compression)) { + return encodeByteOffset(rimage, dataBufferType, mdata, param); + } else if (CbfCompression.NONE.equals(compression)) { + return encodeNone(rimage, dataBufferType, mdata, param); + } else { + throw new RuntimeException("Bug; compression: " + compression); + } + } + + /* + * Specified CbfMetadata indicates the type of data that should be + * encoded. Only encodes to integer (non-floating point) data. Only + * encodes integer (non-floating point) source data with the exception of + * image data of type float which may be used as a signed or unsigned + * 32-bit integer representation in which case the values must fit in a + * signed or unsigned 32-bit integer, respectively, and the deltas must + * fit within a 64-bit signed integer. + */ + private IIOByteBuffer encodeByteOffset(RenderedImage image, int dataBufferType, + CbfMetadata mdata, ImageWriteParam param) { + Rectangle srcBounds = new Rectangle(image.getWidth(), image.getHeight()); + Rectangle srcRegion = param.getSourceRegion(); + srcRegion = (srcRegion == null) ? srcBounds : srcRegion.intersection(srcBounds); + int srcXSubsampOffset = param.getSubsamplingXOffset(); + int srcYSubsampOffset = param.getSubsamplingYOffset(); + srcRegion.translate(srcXSubsampOffset, srcYSubsampOffset); + srcRegion.setSize(srcRegion.width - srcXSubsampOffset, srcRegion.height - srcYSubsampOffset); + int srcRegionMaxY = srcRegion.y + srcRegion.height - 1; + Raster srcRaster = image.getData(srcRegion); + + int srcXSubsamp = param.getSourceXSubsampling(); + int srcYSubsamp = param.getSourceYSubsampling(); + int srcWidth = srcRaster.getWidth(); + int srcHeight = srcRaster.getHeight(); + int srcMaxX = srcRaster.getMinX() + srcWidth - 1; + int srcMaxY = srcRaster.getMinY() + srcHeight - 1; + int[] srcBands = param.getSourceBands(); + int srcBand = (srcBands == null) ? 0 : srcBands[0]; + + int dstWidth = srcWidth / srcXSubsamp; + if (srcWidth % srcXSubsamp != 0) dstWidth++; + int dstHeight = srcHeight / srcYSubsamp; + if (srcHeight % srcYSubsamp != 0) dstHeight++; + + int progressRowPeriod = Math.max(1, (srcRegionMaxY + 1) / 100); + CbfElementType elementType = mdata.getDataElementType(); + + int estimatedSize = (int)((float)dstWidth * (float)dstHeight * 1.5f); + ByteBuffer buffer = ByteBuffer.wrap(new byte[estimatedSize]); + buffer.order(mdata.getDataElementByteOrder()); + + boolean clipped = false; + long value = 0; + long delta = 0; + for (int srcY = srcRaster.getMinY(); srcY <= srcMaxY; srcY += srcYSubsamp) { + if (abortRequested()) { + processWriteAborted(); + return null; + } + if (srcY % progressRowPeriod == 0) { + processImageProgress(((float)srcY / (float)(srcRegionMaxY + 1)) * 100.0f); + } + for (int srcX = srcRaster.getMinX(); srcX <= srcMaxX; srcX += srcXSubsamp) { + long newValue; + switch (elementType) { + case UNSIGNED_8_BIT_INTEGER: + case UNSIGNED_16_BIT_INTEGER: + newValue = srcRaster.getSample(srcX, srcY, srcBand); + if (newValue < 0L) { + newValue = 0L; + clipped = true; + } + break; + case SIGNED_32_BIT_INTEGER: + if (dataBufferType == DataBuffer.TYPE_FLOAT) { + float newValueFloat = srcRaster.getSampleFloat(srcX, srcY, srcBand); + if (newValueFloat < Integer.MIN_VALUE) { + newValue = Integer.MIN_VALUE; + clipped = true; + } else if (newValueFloat > Integer.MAX_VALUE) { + newValue = Integer.MAX_VALUE; + clipped = true; + } else { + newValue = (long)newValueFloat; + if (newValue < Integer.MIN_VALUE) newValue = Integer.MIN_VALUE; + else if (newValue > Integer.MAX_VALUE) newValue = Integer.MAX_VALUE; + } + } else { + newValue = srcRaster.getSample(srcX, srcY, srcBand); + } + break; + case UNSIGNED_32_BIT_INTEGER: + if (dataBufferType == DataBuffer.TYPE_FLOAT) { + float newValueFloat = srcRaster.getSampleFloat(srcX, srcY, srcBand); + if (newValueFloat < 0.0f) { + newValue = 0L; + clipped = true; + } else if (newValueFloat > 4294967295.0f) { + newValue = 4294967295L; + clipped = true; + } else { + newValue = (long)newValueFloat; + if (newValue < 0L) newValue = 0L; + else if (newValue > 4294967295L) newValue = 4294967295L; + } + } else { + newValue = srcRaster.getSample(srcX, srcY, srcBand); + if (newValue < 0L) { + newValue = 0L; + clipped = true; + } + } + break; + default: + newValue = srcRaster.getSample(srcX, srcY, srcBand); + } + delta = newValue - value; + if (Byte.MIN_VALUE < delta && delta <= Byte.MAX_VALUE) { + buffer = ensureRemaining(buffer, 1); + buffer.put((byte)delta); + value = newValue; + continue; + } + buffer = ensureRemaining(buffer, 1); + buffer.put(Byte.MIN_VALUE); + if (Short.MIN_VALUE < delta && delta <= Short.MAX_VALUE) { + buffer = ensureRemaining(buffer, 2); + buffer.putShort((short)delta); + value = newValue; + continue; + } + buffer = ensureRemaining(buffer, 2); + buffer.putShort(Short.MIN_VALUE); + if (Integer.MIN_VALUE < delta && delta <= Integer.MAX_VALUE) { + buffer = ensureRemaining(buffer, 4); + buffer.putInt((int)delta); + value = newValue; + continue; + } + buffer = ensureRemaining(buffer, 4); + buffer.putInt(Integer.MIN_VALUE); + buffer = ensureRemaining(buffer, 8); + buffer.putLong(delta); + value = newValue; + } + } + if (clipped) { + processWarningOccurred(0, "Source value(s) clipped to fit in destination element type"); + } + + IIOByteBuffer result = new IIOByteBuffer(buffer.array(), 0, buffer.limit()); + + mdata.setDataCompression(CbfCompression.BYTE_OFFSET); + mdata.setDataWidth(dstWidth); + mdata.setDataHeight(dstHeight); + mdata.setDataNumElements(dstWidth * dstHeight); + mdata.setDataSize(result.getLength()); + MessageDigest digest = null; + try { + digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + processWarningOccurred(0, "Failed to find MD5 algorithm for data MD5 digest calculation"); + } + if (digest != null) { + digest.update(result.getData(), result.getOffset(), result.getLength()); + mdata.setDataMd5(digest.digest()); + } + + return result; + } + + private ByteBuffer ensureRemaining(ByteBuffer buffer, int remaining) { + if (buffer.remaining() >= remaining) return buffer; + int capacity = buffer.capacity(); + int extraCapacity = capacity / 3; + int minExtraCapacity = remaining - buffer.remaining(); + if (extraCapacity < minExtraCapacity) extraCapacity = minExtraCapacity; + int newCapacity = capacity + extraCapacity; + byte[] newArray = Arrays.copyOf(buffer.array(), newCapacity); + ByteBuffer newBuffer = ByteBuffer.wrap(newArray); + newBuffer.position(buffer.position()); + newBuffer.order(buffer.order()); + return newBuffer; + } + + /* + * Specified CbfMetadata indicates the type of data that should be + * encoded. + */ + private IIOByteBuffer encodeNone(RenderedImage image, int dataBufferType, CbfMetadata mdata, + ImageWriteParam param) { + Rectangle srcBounds = new Rectangle(image.getWidth(), image.getHeight()); + Rectangle srcRegion = param.getSourceRegion(); + srcRegion = (srcRegion == null) ? srcBounds : srcRegion.intersection(srcBounds); + int srcXSubsampOffset = param.getSubsamplingXOffset(); + int srcYSubsampOffset = param.getSubsamplingYOffset(); + srcRegion.translate(srcXSubsampOffset, srcYSubsampOffset); + srcRegion.setSize(srcRegion.width - srcXSubsampOffset, srcRegion.height - srcYSubsampOffset); + int srcRegionMaxY = srcRegion.y + srcRegion.height - 1; + Raster srcRaster = image.getData(srcRegion); + + int srcXSubsamp = param.getSourceXSubsampling(); + int srcYSubsamp = param.getSourceYSubsampling(); + int srcWidth = srcRaster.getWidth(); + int srcHeight = srcRaster.getHeight(); + int srcMaxX = srcRaster.getMinX() + srcWidth - 1; + int srcMaxY = srcRaster.getMinY() + srcHeight - 1; + int[] srcBands = param.getSourceBands(); + int srcBand = (srcBands == null) ? 0 : srcBands[0]; + + int dstWidth = srcWidth / srcXSubsamp; + if (srcWidth % srcXSubsamp != 0) dstWidth++; + int dstHeight = srcHeight / srcYSubsamp; + if (srcHeight % srcYSubsamp != 0) dstHeight++; + + int progressRowPeriod = Math.max(1, (srcRegionMaxY + 1) / 100); + CbfElementType elementType = mdata.getDataElementType(); + int numBytesPerElement; + switch (elementType) { + case UNSIGNED_8_BIT_INTEGER: + case SIGNED_8_BIT_INTEGER: + numBytesPerElement = 1; + break; + case UNSIGNED_16_BIT_INTEGER: + case SIGNED_16_BIT_INTEGER: + numBytesPerElement = 2; + break; + case UNSIGNED_32_BIT_INTEGER: + case SIGNED_32_BIT_INTEGER: + case SIGNED_32_BIT_REAL_IEEE: + numBytesPerElement = 4; + break; + case SIGNED_64_BIT_REAL_IEEE: + numBytesPerElement = 8; + break; + default: + throw new RuntimeException("Bug; element type: " + elementType); + } + + int dataSize = dstWidth * dstHeight * numBytesPerElement; + ByteBuffer buffer = ByteBuffer.wrap(new byte[dataSize]); + buffer.order(mdata.getDataElementByteOrder()); + + boolean clipped = false; + for (int srcY = srcRaster.getMinY(); srcY <= srcMaxY; srcY += srcYSubsamp) { + if (abortRequested()) { + processWriteAborted(); + return null; + } + if (srcY % progressRowPeriod == 0) { + processImageProgress(((float)srcY / (float)(srcRegionMaxY + 1)) * 100.0f); + } + for (int srcX = srcRaster.getMinX(); srcX <= srcMaxX; srcX += srcXSubsamp) { + switch (elementType) { + case UNSIGNED_8_BIT_INTEGER: + int ubyteValue = srcRaster.getSample(srcX, srcY, srcBand); + if (ubyteValue < 0) { + ubyteValue = 0; + clipped = true; + } + buffer.put((byte)ubyteValue); + break; + case SIGNED_8_BIT_INTEGER: + buffer.put((byte)srcRaster.getSample(srcX, srcY, srcBand)); + break; + case UNSIGNED_16_BIT_INTEGER: + int ushortValue = srcRaster.getSample(srcX, srcY, srcBand); + if (ushortValue < 0) { + ushortValue = 0; + clipped = true; + } + buffer.putShort((short)ushortValue); + break; + case SIGNED_16_BIT_INTEGER: + buffer.putShort((short)srcRaster.getSample(srcX, srcY, srcBand)); + break; + case UNSIGNED_32_BIT_INTEGER: + if (dataBufferType == DataBuffer.TYPE_FLOAT) { + float floatValue = srcRaster.getSampleFloat(srcX, srcY, srcBand); + long longValue; + if (floatValue < 0.0f) { + longValue = 0L; + clipped = true; + } else if (floatValue > 4294967295.0f) { + longValue = 4294967295L; + clipped = true; + } else { + longValue = (long)floatValue; + if (longValue < 0L) longValue = 0L; + else if (longValue > 4294967295L) longValue = 4294967295L; + } + buffer.putInt((int)longValue); + } else { + int uintValue = srcRaster.getSample(srcX, srcY, srcBand); + if (uintValue < 0) { + uintValue = 0; + clipped = true; + } + buffer.putInt(uintValue); + } + break; + case SIGNED_32_BIT_INTEGER: + if (dataBufferType == DataBuffer.TYPE_FLOAT) { + float floatValue = srcRaster.getSampleFloat(srcX, srcY, srcBand); + long longValue; + if (floatValue < Integer.MIN_VALUE) { + longValue = Integer.MIN_VALUE; + clipped = true; + } else if (floatValue > Integer.MAX_VALUE) { + longValue = Integer.MAX_VALUE; + clipped = true; + } else { + longValue = (long)floatValue; + if (longValue < Integer.MIN_VALUE) longValue = Integer.MIN_VALUE; + else if (longValue > Integer.MAX_VALUE) longValue = Integer.MAX_VALUE; + } + buffer.putInt((int)longValue); + } else { + buffer.putInt(srcRaster.getSample(srcX, srcY, srcBand)); + } + break; + case SIGNED_32_BIT_REAL_IEEE: + buffer.putFloat(srcRaster.getSampleFloat(srcX, srcY, srcBand)); + break; + case SIGNED_64_BIT_REAL_IEEE: + buffer.putDouble(srcRaster.getSampleDouble(srcX, srcY, srcBand)); + break; + default: + throw new RuntimeException("Bug; element type: " + elementType); + } + } + } + if (clipped) { + processWarningOccurred(0, "Source value(s) clipped to fit in destination element type"); + } + + IIOByteBuffer result = new IIOByteBuffer(buffer.array(), 0, buffer.limit()); + + mdata.setDataCompression(CbfCompression.NONE); + mdata.setDataWidth(dstWidth); + mdata.setDataHeight(dstHeight); + mdata.setDataNumElements(dstWidth * dstHeight); + mdata.setDataSize(result.getLength()); + MessageDigest digest = null; + try { + digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + processWarningOccurred(0, "Failed to find MD5 algorithm for data MD5 digest calculation"); + } + if (digest != null) { + digest.update(result.getData(), result.getOffset(), result.getLength()); + mdata.setDataMd5(digest.digest()); + } + + return result; + } + + private ImageOutputStream getNonNullOutput() { + Object output = getOutput(); + if (output == null) throw new IllegalStateException("No output stream"); + return (ImageOutputStream)output; + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriterSpi.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriterSpi.java new file mode 100644 index 0000000..e5725b4 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfImageWriterSpi.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Locale; + +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriter; +import javax.imageio.spi.ImageWriterSpi; + +/** + * I am a writer SPI for CBF images. + */ +public class CbfImageWriterSpi extends ImageWriterSpi { + private static final String VENDOR_NAME = "IMCA-CAT"; + private static final String VERSION = Version.ID; + private static final String[] FORMAT_NAMES = { "cbf", "CBF" }; + private static final String[] FORMAT_SUFFIXES = { "cbf" }; + private static final String[] MIME_TYPES = { "image/x-cbf" }; + private static final String WRITER_CLASS_NAME = "imcacat.jcbf.CbfImageWriter"; + private static final String[] READER_SPI_CLASS_NAMES = { "imcacat.jcbf.CbfImageReaderSpi" }; + private static final String METADATA_FORMAT_NAME = "imcacat.jcbf.CbfMetadata_1.0"; + private static final String METADATA_FORMAT_CLASS_NAME = "imcacat.jcbf.CbfMetadataFormat"; + + public CbfImageWriterSpi() { + super(VENDOR_NAME, VERSION, FORMAT_NAMES, FORMAT_SUFFIXES, MIME_TYPES, WRITER_CLASS_NAME, + STANDARD_OUTPUT_TYPE, READER_SPI_CLASS_NAMES, false, null, null, null, null, false, + METADATA_FORMAT_NAME, METADATA_FORMAT_CLASS_NAME, null, null); + } + + @Override + public boolean canEncodeImage(ImageTypeSpecifier type) { + return type.getBufferedImageType() == BufferedImage.TYPE_BYTE_BINARY + || (type.getNumBands() == 1 && type.getNumComponents() == 1); + } + + @Override + public ImageWriter createWriterInstance(Object extension) throws IOException { + return new CbfImageWriter(this); + } + + @Override + public String getDescription(Locale locale) { + return "CBF image writer"; + } + +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfMetadata.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfMetadata.java new file mode 100644 index 0000000..4190daa --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfMetadata.java @@ -0,0 +1,760 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.nio.ByteOrder; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; + +import org.w3c.dom.Node; + +/** + * I am the metadata for a CBF image. + */ +public class CbfMetadata extends IIOMetadata { + public static final String NATIVE_FORMAT_NAME = "imcacat.jcbf.CbfMetadataFormat_1.0"; + + private static final Pattern VALID_STRING = Pattern.compile("(?:\\p{Graph}|\\p{Blank})*"); + private static final Pattern HEADER_DETECTOR_COMMA_DELIM = Pattern.compile("\\s*,\\s*"); + private static final Pattern HEADER_DETECTOR_SN_DELIM = Pattern.compile("\\s+SN:\\s*"); + + private String identifier = "###CBF: VERSION 1.5, CBFlib v0.7.8 - SLS/DECTRIS PILATUS detectors"; + private String dataBlockName = "image"; + + private String headerConvention = "SLS_1.0"; + private String headerDetector; + private String headerDetectorName; + private String headerDetectorSerialNumber; + private String headerDetectorDescription; + private Date headerDate; + private float[] headerPixelSize; + private String headerSensorType; + private Float headerSensorThickness; + private Float headerExposureTime; + private Float headerExposurePeriod; + private Float headerTau; + private Integer headerCountCutoff; + private Float headerThreshold; + private String headerGainType; + private Float headerGainVrf; + private Integer headerNumExcludedPixels; + private String headerExcludedPixels; + private String headerFlatField; + private String headerTrim; + private Float headerWavelength; + private float[] headerEnergyRange; + private Float headerDetectorDistance; + private Float headerDetectorVOffset; + private float[] headerBeamXY; + private Float headerFlux; + private Float headerFilterTransmission; + private Float headerStartAngle; + private Float headerAngleIncrement; + private Float headerDetector2Theta; + private Float headerPolarization; + private Float headerAlpha; + private Float headerKappa; + private Float headerPhi; + private Float headerChi; + private String headerOscillationAxis; + private Integer headerNumOscillations; + private String headerComment; + private List headerUnrecognizedLines = new ArrayList(); + + private CbfCompression dataCompression; + private int dataSize; + private int dataId; + private CbfElementType dataElementType = CbfElementType.SIGNED_32_BIT_INTEGER; + private ByteOrder dataElementByteOrder = ByteOrder.LITTLE_ENDIAN; + private byte[] dataMd5; + private int dataNumElements; + private int dataWidth; + private int dataHeight; + private int dataPadding = 4095; + + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public Node getAsTree(String formatName) { + if (!NATIVE_FORMAT_NAME.equals(formatName)) { + throw new IllegalArgumentException("Format name: " + formatName); + } + Node result = new IIOMetadataNode(NATIVE_FORMAT_NAME); + if (identifier != null) { + result.appendChild(createAssociation("identifier", identifier)); + } + if (dataBlockName != null) { + result.appendChild(createAssociation("dataBlockName", dataBlockName)); + } + + if (headerConvention != null) { + result.appendChild(createAssociation("headerConvention", headerConvention)); + } + if (headerDetector != null) { + result.appendChild(createAssociation("headerDetector", headerDetector)); + } + if (headerDate != null) { + DateFormat format = new SimpleDateFormat("yyyy-MMM-dd'T'HH:mm:ss.SSS", Locale.US); + result.appendChild(createAssociation("headerDate", format.format(headerDate))); + } + if (headerPixelSize != null) { + result.appendChild(createAssociation("headerPixelSize", String.format(Locale.US, + "%.0fe-6 m x %.0fe-6 m", headerPixelSize[0] * 1e6f, headerPixelSize[1] * 1e6f))); + } + if (headerSensorType != null) { + result.appendChild(createAssociation("headerSensorType", headerSensorType)); + } + if (headerSensorThickness != null) { + result.appendChild(createAssociation("headerSensorThickness", + String.format(Locale.US, "%.6f m", headerSensorThickness))); + } + if (headerExposureTime != null) { + result.appendChild(createAssociation("headerExposureTime", + String.format(Locale.US, "%.6f s", headerExposureTime))); + } + if (headerExposurePeriod != null) { + result.appendChild(createAssociation("headerExposurePeriod", + String.format(Locale.US, "%.6f s", headerExposurePeriod))); + } + if (headerTau != null) { + result.appendChild(createAssociation("headerTau", + String.format(Locale.US, "%.1fe-09 s", 1e9f * headerTau))); + } + if (headerCountCutoff != null) { + result.appendChild(createAssociation("headerCountCutoff", + String.format(Locale.US, "%d counts", headerCountCutoff))); + } + if (headerThreshold != null) { + result.appendChild(createAssociation("headerThreshold", + String.format(Locale.US, "%.0f eV", headerThreshold))); + } + if (headerGainType != null) { + result.appendChild(createAssociation("headerGainType", headerGainType)); + } + if (headerGainVrf != null) { + result.appendChild(createAssociation("headerGainVrf", + String.format(Locale.US, "%.3f", headerGainVrf))); + } + if (headerNumExcludedPixels != null) { + result.appendChild(createAssociation("headerNumExcludedPixels", headerExcludedPixels)); + } + if (headerExcludedPixels != null) { + result.appendChild(createAssociation("headerExcludedPixels", headerExcludedPixels)); + } + if (headerFlatField != null) { + result.appendChild(createAssociation("headerFlatField", headerFlatField)); + } + if (headerTrim != null) { + result.appendChild(createAssociation("headerTrim", headerTrim)); + } + if (headerWavelength != null) { + result.appendChild(createAssociation("headerWavelength", + String.format(Locale.US, "%.4f A", headerWavelength))); + } + if (headerEnergyRange != null) { + result.appendChild(createAssociation("headerEnergyRange", + String.format(Locale.US, "(%.0f, %.0f) eV", headerEnergyRange[0], headerEnergyRange[1]))); + } + if (headerDetectorDistance != null) { + result.appendChild(createAssociation("headerDetectorDistance", + String.format(Locale.US, "%.5f m", headerDetectorDistance))); + } + if (headerDetectorVOffset != null) { + result.appendChild(createAssociation("headerDetectorVOffset", + String.format(Locale.US, "%.5f m", headerDetectorVOffset))); + } + if (headerBeamXY != null) { + result.appendChild(createAssociation("headerBeamXY", + String.format(Locale.US, "(%.2f, %.2f) pixels", headerBeamXY[0], headerBeamXY[1]))); + } + if (headerFlux != null) { + result.appendChild(createAssociation("headerFlux", + String.format(Locale.US, "%.4f ph/s", headerFlux))); + } + if (headerFilterTransmission != null) { + result.appendChild(createAssociation("headerFilterTransmission", + String.format(Locale.US, "%.4f", headerFilterTransmission))); + } + if (headerStartAngle != null) { + result.appendChild(createAssociation("headerStartAngle", + String.format(Locale.US, "%.4f deg.", headerStartAngle))); + } + if (headerAngleIncrement != null) { + result.appendChild(createAssociation("headerAngleIncrement", + String.format(Locale.US, "%.4f deg.", headerAngleIncrement))); + } + if (headerDetector2Theta != null) { + result.appendChild(createAssociation("headerDetector2Theta", + String.format(Locale.US, "%.4f deg.", headerDetector2Theta))); + } + if (headerPolarization != null) { + result.appendChild(createAssociation("headerPolarization", + String.format(Locale.US, "%.3f", headerPolarization))); + } + if (headerAlpha != null) { + result.appendChild(createAssociation("headerAlpha", + String.format(Locale.US, "%.4f deg.", headerAlpha))); + } + if (headerKappa != null) { + result.appendChild(createAssociation("headerKappa", + String.format(Locale.US, "%.4f deg.", headerKappa))); + } + if (headerPhi != null) { + result.appendChild(createAssociation("headerPhi", + String.format(Locale.US, "%.4f deg.", headerPhi))); + } + if (headerChi != null) { + result.appendChild(createAssociation("headerChi", + String.format(Locale.US, "%.4f deg.", headerChi))); + } + if (headerOscillationAxis != null) { + result.appendChild(createAssociation("headerOscillationAxis", headerOscillationAxis)); + } + if (headerNumOscillations != null) { + result.appendChild(createAssociation("headerNumOscillations", + String.format(Locale.US, "%d", headerNumOscillations))); + } + if (headerComment != null) { + result.appendChild(createAssociation("headerComment", headerComment)); + } + int i = 0; + for (String each : getHeaderUnrecognizedLines()) { + result.appendChild(createAssociation("headerUnrecognized" + i, each)); + i++; + } + + if (dataCompression != null) { + result.appendChild(createAssociation("dataCompression", + dataCompression.toContentTypeConversions())); + } + result.appendChild(createAssociation("dataSize", Integer.toString(dataSize))); + result.appendChild(createAssociation("dataId", Integer.toString(dataId))); + if (dataElementType != null) { + result.appendChild(createAssociation("dataElementType", + dataElementType.toXBinaryElementType())); + } + if (dataElementByteOrder != null) { + result + .appendChild(createAssociation("dataElementByteOrder", dataElementByteOrder.toString())); + } + if (dataMd5 != null) { + result.appendChild(createAssociation("dataMd5", Base64.encode(dataMd5))); + } + result.appendChild(createAssociation("dataNumElements", Integer.toString(dataNumElements))); + result.appendChild(createAssociation("dataWidth", Integer.toString(dataWidth))); + result.appendChild(createAssociation("dataHeight", Integer.toString(dataHeight))); + result.appendChild(createAssociation("dataPadding", Integer.toString(dataPadding))); + + return result; + } + + private IIOMetadataNode createAssociation(String key, String value) { + IIOMetadataNode result = new IIOMetadataNode("association"); + result.setAttribute("key", key); + result.setAttribute("value", value); + return result; + } + + @Override + public void mergeTree(String formatName, Node root) throws IIOInvalidTreeException { + throw new IllegalStateException("Read-only"); + } + + @Override + public void reset() { + throw new IllegalStateException("Read-only"); + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String newIdentifier) { + validate(newIdentifier); + identifier = newIdentifier; + } + + public String getDataBlockName() { + return dataBlockName; + } + + public void setDataBlockName(String newDataBlockName) { + validate(newDataBlockName); + dataBlockName = newDataBlockName; + } + + public String getHeaderConvention() { + return headerConvention; + } + + public void setHeaderConvention(String newHeaderConvention) { + validate(newHeaderConvention); + headerConvention = newHeaderConvention; + } + + public String getHeaderDetector() { + return headerDetector; + } + + public void setHeaderDetector(String headerDetector) { + validate(headerDetector); + this.headerDetector = headerDetector; + updateHeaderDetectorParts(headerDetector); + } + + private void updateHeaderDetectorParts(String headerDetector) { + headerDetectorName = null; + headerDetectorSerialNumber = null; + headerDetectorDescription = null; + if (headerDetector == null) return; + String[] parts = HEADER_DETECTOR_COMMA_DELIM.split(headerDetector); + if (parts.length > 1) { + headerDetectorName = parts[0]; + headerDetectorSerialNumber = parts[1]; + if (parts.length > 2) headerDetectorDescription = parts[2]; + return; + } + parts = HEADER_DETECTOR_SN_DELIM.split(headerDetector); + if (parts.length > 1) { + headerDetectorName = parts[0]; + headerDetectorSerialNumber = parts[1]; + } + } + + public String getHeaderDetectorName() { + return headerDetectorName; + } + + public String getHeaderDetectorSerialNumber() { + return headerDetectorSerialNumber; + } + + public String getHeaderDetectorDescription() { + return headerDetectorDescription; + } + + public Date getHeaderDate() { + return headerDate; + } + + public void setHeaderDate(Date headerDate) { + this.headerDate = headerDate; + } + + public float[] getHeaderPixelSize() { + return headerPixelSize; + } + + public void setHeaderPixelSize(float[] headerPixelSize) { + this.headerPixelSize = headerPixelSize; + } + + public String getHeaderSensorType() { + return headerSensorType; + } + + public void setHeaderSensorType(String headerSensorType) { + validate(headerSensorType); + this.headerSensorType = headerSensorType; + } + + public Float getHeaderSensorThickness() { + return headerSensorThickness; + } + + public void setHeaderSensorThickness(Float headerSensorThickness) { + this.headerSensorThickness = headerSensorThickness; + } + + public Float getHeaderExposureTime() { + return headerExposureTime; + } + + public void setHeaderExposureTime(Float headerExposureTime) { + this.headerExposureTime = headerExposureTime; + } + + public Float getHeaderExposurePeriod() { + return headerExposurePeriod; + } + + public void setHeaderExposurePeriod(Float headerExposurePeriod) { + this.headerExposurePeriod = headerExposurePeriod; + } + + public Float getHeaderTau() { + return headerTau; + } + + public void setHeaderTau(Float headerTau) { + this.headerTau = headerTau; + } + + public Integer getHeaderCountCutoff() { + return headerCountCutoff; + } + + public void setHeaderCountCutoff(Integer headerCountCutoff) { + this.headerCountCutoff = headerCountCutoff; + } + + public Float getHeaderThreshold() { + return headerThreshold; + } + + public void setHeaderThreshold(Float headerThreshold) { + this.headerThreshold = headerThreshold; + } + + public String getHeaderGainType() { + return headerGainType; + } + + public void setHeaderGainType(String headerGainType) { + validate(headerGainType); + this.headerGainType = headerGainType; + } + + public Float getHeaderGainVrf() { + return headerGainVrf; + } + + public void setHeaderGainVrf(Float headerGainVrf) { + this.headerGainVrf = headerGainVrf; + } + + public Integer getHeaderNumExcludedPixels() { + return headerNumExcludedPixels; + } + + public void setHeaderNumExcludedPixels(Integer headerNumExcludedPixels) { + this.headerNumExcludedPixels = headerNumExcludedPixels; + } + + public String getHeaderExcludedPixels() { + return headerExcludedPixels; + } + + public void setHeaderExcludedPixels(String headerExcludedPixels) { + validate(headerExcludedPixels); + this.headerExcludedPixels = headerExcludedPixels; + } + + public String getHeaderFlatField() { + return headerFlatField; + } + + public void setHeaderFlatField(String headerFlatField) { + validate(headerFlatField); + this.headerFlatField = headerFlatField; + } + + public String getHeaderTrim() { + return headerTrim; + } + + public void setHeaderTrim(String headerTrim) { + validate(headerTrim); + this.headerTrim = headerTrim; + } + + public Float getHeaderWavelength() { + return headerWavelength; + } + + public void setHeaderWavelength(Float headerWavelength) { + this.headerWavelength = headerWavelength; + } + + public float[] getHeaderEnergyRange() { + return headerEnergyRange; + } + + public void setHeaderEnergyRange(float[] headerEnergyRange) { + this.headerEnergyRange = headerEnergyRange; + } + + public Float getHeaderDetectorDistance() { + return headerDetectorDistance; + } + + public void setHeaderDetectorDistance(Float headerDetectorDistance) { + this.headerDetectorDistance = headerDetectorDistance; + } + + public Float getHeaderDetectorVOffset() { + return headerDetectorVOffset; + } + + public void setHeaderDetectorVOffset(Float headerDetectorVOffset) { + this.headerDetectorVOffset = headerDetectorVOffset; + } + + public float[] getHeaderBeamXY() { + return headerBeamXY; + } + + public void setHeaderBeamXY(float[] headerBeamXY) { + this.headerBeamXY = headerBeamXY; + } + + public Float getHeaderFlux() { + return headerFlux; + } + + public void setHeaderFlux(Float headerFlux) { + this.headerFlux = headerFlux; + } + + public Float getHeaderFilterTransmission() { + return headerFilterTransmission; + } + + public void setHeaderFilterTransmission(Float headerFilterTransmission) { + this.headerFilterTransmission = headerFilterTransmission; + } + + public Float getHeaderStartAngle() { + return headerStartAngle; + } + + public void setHeaderStartAngle(Float headerStartAngle) { + this.headerStartAngle = headerStartAngle; + } + + public Float getHeaderAngleIncrement() { + return headerAngleIncrement; + } + + public void setHeaderAngleIncrement(Float headerAngleIncrement) { + this.headerAngleIncrement = headerAngleIncrement; + } + + public Float getHeaderDetector2Theta() { + return headerDetector2Theta; + } + + public void setHeaderDetector2Theta(Float headerDetector2Theta) { + this.headerDetector2Theta = headerDetector2Theta; + } + + public Float getHeaderPolarization() { + return headerPolarization; + } + + public void setHeaderPolarization(Float headerPolarization) { + this.headerPolarization = headerPolarization; + } + + public Float getHeaderAlpha() { + return headerAlpha; + } + + public void setHeaderAlpha(Float headerAlpha) { + this.headerAlpha = headerAlpha; + } + + public Float getHeaderKappa() { + return headerKappa; + } + + public void setHeaderKappa(Float headerKappa) { + this.headerKappa = headerKappa; + } + + public Float getHeaderPhi() { + return headerPhi; + } + + public void setHeaderPhi(Float headerPhi) { + this.headerPhi = headerPhi; + } + + public Float getHeaderChi() { + return headerChi; + } + + public void setHeaderChi(Float headerChi) { + this.headerChi = headerChi; + } + + public String getHeaderOscillationAxis() { + return headerOscillationAxis; + } + + public void setHeaderOscillationAxis(String headerOscillationAxis) { + validate(headerOscillationAxis); + this.headerOscillationAxis = headerOscillationAxis; + } + + public Integer getHeaderNumOscillations() { + return headerNumOscillations; + } + + public void setHeaderNumOscillations(Integer headerNumOscillations) { + this.headerNumOscillations = headerNumOscillations; + } + + public String getHeaderComment() { + return headerComment; + } + + public void setHeaderComment(String headerComment) { + validate(headerComment); + this.headerComment = headerComment; + } + + public void setHeaderDefaults() { + headerDetector = "PILATUS 6M"; + headerDate = new Date(); + headerPixelSize = new float[] { 172e-6f, 172e-6f }; + headerSensorType = "Silicon"; + headerSensorThickness = 0.000320f; + headerExposureTime = 1.0f; + headerExposurePeriod = 1.0f; + headerTau = 124.0e-09f; + headerCountCutoff = 1048575; + headerThreshold = 7439.0f; + headerGainType = "not implemented"; + headerGainVrf = 9.9f; + headerWavelength = 1.0f; + headerEnergyRange = new float[] { 0.0f, 0.0f }; + headerDetectorDistance = 0.2f; + headerDetectorVOffset = 0.0f; + headerBeamXY = new float[] { 1231.5f, 1263.5f }; + headerFlux = 0.0f; + headerFilterTransmission = 0.0f; + headerStartAngle = 0.0f; + headerAngleIncrement = 0.0f; + headerDetector2Theta = 0.0f; + headerPolarization = 0.990f; + headerAlpha = 0.0f; + headerKappa = 0.0f; + headerPhi = 0.0f; + headerChi = 0.0f; + headerOscillationAxis = "X, CW"; + headerNumOscillations = 1; + } + + public List getHeaderUnrecognizedLines() { + return Collections.unmodifiableList(headerUnrecognizedLines); + } + + public void addHeaderUnrecognizedLine(String line) { + validate(line); + headerUnrecognizedLines.add(line); + } + + public CbfCompression getDataCompression() { + return dataCompression; + } + + public void setDataCompression(CbfCompression newDataCompression) { + dataCompression = newDataCompression; + } + + public int getDataSize() { + return dataSize; + } + + public void setDataSize(int newDataSize) { + dataSize = newDataSize; + } + + public int getDataId() { + return dataId; + } + + public void setDataId(int newDataId) { + dataId = newDataId; + } + + public CbfElementType getDataElementType() { + return dataElementType; + } + + public void setDataElementType(CbfElementType newDataElementType) { + dataElementType = newDataElementType; + } + + public ByteOrder getDataElementByteOrder() { + return dataElementByteOrder; + } + + public void setDataElementByteOrder(ByteOrder newDataElementByteOrder) { + dataElementByteOrder = newDataElementByteOrder; + } + + public byte[] getDataMd5() { + return dataMd5; + } + + public void setDataMd5(byte[] newDataMd5) { + dataMd5 = newDataMd5; + } + + public int getDataNumElements() { + return dataNumElements; + } + + public void setDataNumElements(int newDataNumElements) { + dataNumElements = newDataNumElements; + } + + public int getDataWidth() { + return dataWidth; + } + + public void setDataWidth(int newDataWidth) { + dataWidth = newDataWidth; + } + + public int getDataHeight() { + return dataHeight; + } + + public void setDataHeight(int newDataHeight) { + dataHeight = newDataHeight; + } + + public int getDataPadding() { + return dataPadding; + } + + public void setDataPadding(int newDataPadding) { + dataPadding = newDataPadding; + } + + private void validate(String s) { + if (s == null) return; + if (VALID_STRING.matcher(s).matches()) return; + throw new IllegalArgumentException("String contains an illegal character"); + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfMetadataFormat.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfMetadataFormat.java new file mode 100644 index 0000000..8c9b6b8 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/CbfMetadataFormat.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadataFormatImpl; + +/** + * I am the metadata format for a CBF image. + */ +public class CbfMetadataFormat extends IIOMetadataFormatImpl { + private static final String ASSOCIATION_ELEMENT_NAME = "association"; + + public CbfMetadataFormat() { + super(CbfMetadata.NATIVE_FORMAT_NAME, CHILD_POLICY_REPEAT); + addElement(ASSOCIATION_ELEMENT_NAME, CbfMetadata.NATIVE_FORMAT_NAME, CHILD_POLICY_EMPTY); + addAttribute(ASSOCIATION_ELEMENT_NAME, "key", DATATYPE_STRING, true, null); + addAttribute(ASSOCIATION_ELEMENT_NAME, "value", DATATYPE_STRING, true, null); + } + + @Override + public boolean canNodeAppear(String elementName, ImageTypeSpecifier imageType) { + return ASSOCIATION_ELEMENT_NAME.equals(elementName); + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ImageInputStreamCbfData.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ImageInputStreamCbfData.java new file mode 100644 index 0000000..bed91cc --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ImageInputStreamCbfData.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.io.IOException; +import java.nio.ByteOrder; + +import javax.imageio.stream.ImageInputStream; + +/** + * I am a CbfData backed by an ImageInputStream. + */ +public class ImageInputStreamCbfData implements CbfData { + private ImageInputStream stream; + + public ImageInputStreamCbfData(ImageInputStream stream) { + this.stream = stream; + } + + @Override + public void order(ByteOrder bo) { + stream.setByteOrder(bo); + } + + @Override + public byte getByte() throws IOException { + return stream.readByte(); + } + + @Override + public short getUbyte() throws IOException { + return (short)stream.readUnsignedByte(); + } + + @Override + public short getShort() throws IOException { + return stream.readShort(); + } + + @Override + public int getUshort() throws IOException { + return stream.readUnsignedShort(); + } + + @Override + public int getInt() throws IOException { + return stream.readInt(); + } + + @Override + public long getUint() throws IOException { + return stream.readUnsignedInt(); + } + + @Override + public long getLong() throws IOException { + return stream.readLong(); + } + + @Override + public float getFloat() throws IOException { + return stream.readFloat(); + } + + @Override + public double getDouble() throws IOException { + return stream.readDouble(); + } + + @Override + public void skipBytes(int count) throws IOException { + stream.skipBytes(count); + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ImageJCbfReader.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ImageJCbfReader.java new file mode 100644 index 0000000..c531e79 --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/ImageJCbfReader.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferFloat; +import java.io.File; +import java.io.IOException; +import java.util.Iterator; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.stream.ImageInputStream; + +import ij.IJ; +import ij.ImagePlus; +import ij.io.OpenDialog; +import ij.plugin.PlugIn; +import ij.process.FloatProcessor; +import ij.process.ImageProcessor; + +/** + * I am an ImageJ plug-in for reading a CBF image. + */ +public class ImageJCbfReader implements PlugIn { + private static final Pattern WHITESPACE_PATTERN = Pattern.compile(" "); + + @Override + public void run(String arg) { + OpenDialog dialog = new OpenDialog("Open image ...", arg); + if (dialog.getFileName() == null) return; + ImagePlus image = read(new File(dialog.getDirectory(), dialog.getFileName())); + if (image == null) return; + image.show(); + } + + private ImagePlus read(File cbfFile) { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + ImageInputStream stream = null; + try { + stream = ImageIO.createImageInputStream(cbfFile); + } catch (IOException e) { + showError("Error creating input stream from specified file", e); + return null; + } + try { + if (!spi.canDecodeInput(stream)) { + showMessage("The specified file is not a CBF image."); + return null; + } + } catch (IOException e) { + showError("Error checking whether specified file is a CBF image", e); + return null; + } + CbfImageReader reader = new CbfImageReader(spi); + reader.setInput(stream); + ImageReadParam param = reader.getDefaultReadParam(); + Iterator types = null; + try { + types = reader.getImageTypes(0); + } catch (IOException e) { + showError("Error reading CBF image metadata", e); + reader.dispose(); + return null; + } + while (types.hasNext()) { + ImageTypeSpecifier each = types.next(); + if (each.getSampleModel().getDataType() == DataBuffer.TYPE_FLOAT) { + param.setDestinationType(each); + break; + } + } + + final String cbfFilePath = cbfFile.getPath(); + reader.addIIOReadWarningListener(new IIOReadWarningListener() { + @Override + public void warningOccurred(ImageReader source, String warning) { + IJ.log("Warning: " + warning + " (" + cbfFilePath + ")"); + } + }); + + int width = 0; + int height = 0; + try { + width = reader.getWidth(0); + height = reader.getHeight(0); + } catch (IOException e) { + showError("Error determining width and height of CBF image", e); + reader.dispose(); + return null; + } + BufferedImage image = null; + try { + image = reader.read(0, param); + } catch (IOException e) { + showError("Error reading CBF image data", e); + reader.dispose(); + return null; + } + DataBuffer buffer = image.getData().getDataBuffer(); + int bufferType = buffer.getDataType(); + float[] pixels; + ImageProcessor ip; + if (buffer instanceof DataBufferFloat) { + pixels = ((DataBufferFloat)buffer).getData(); + ip = new FloatProcessor(width, height, pixels, null); + image.flush(); + reader.dispose(); + return new ImagePlus(cbfFile.getName(), ip); + } else if (bufferType == DataBuffer.TYPE_INT || bufferType == DataBuffer.TYPE_FLOAT) { + pixels = new float[width * height]; + for (int i = 0; i < pixels.length; i++) + pixels[i] = buffer.getElemFloat(i); + ip = new FloatProcessor(width, height, pixels, null); + image.flush(); + reader.dispose(); + return new ImagePlus(cbfFile.getName(), ip); + } else if (bufferType == DataBuffer.TYPE_DOUBLE) { + pixels = new float[width * height]; + boolean clipped = false; + for (int i = 0; i < pixels.length; i++) { + double doubleValue = buffer.getElemDouble(i); + float floatValue; + if (doubleValue < Float.MIN_VALUE) { + floatValue = Float.MIN_VALUE; + clipped = true; + } else if (doubleValue > Float.MAX_VALUE) { + floatValue = Float.MAX_VALUE; + clipped = true; + } else { + floatValue = (float)doubleValue; + } + pixels[i] = floatValue; + } + if (clipped) { + IJ.log("Warning: pixel value(s) clipped to fit in type float (" + cbfFilePath + ")"); + } + ip = new FloatProcessor(width, height, pixels, null); + image.flush(); + reader.dispose(); + return new ImagePlus(cbfFile.getName(), ip); + } else { + reader.dispose(); + return new ImagePlus(cbfFile.getName(), image); + } + } + + private static void showError(String msg, Throwable t) { + StringBuffer buffer = new StringBuffer(); + buffer.append(msg); + if (t != null) { + buffer.append(": "); + buffer.append(t.getMessage()); + } + if (buffer.length() != 0) { + char last = buffer.charAt(buffer.length() - 1); + if (last != '.' && last != '!' && last != '?') buffer.append('.'); + } + IJ.error("CBF Reader", wordWrap(buffer.toString(), 78)); + } + + private static void showMessage(String msg) { + IJ.showMessage("CBF Reader", wordWrap(msg, 78)); + } + + private static String wordWrap(String text, int maxColumnWidth) { + return wordWrap(text, maxColumnWidth, System.getProperty("line.separator")); + } + + /* + * Performs a very basic type of word wrapping on the specified text. + * Splits words on the space character and expects the text to not contain + * any whitespace characters other than the space character. + */ + private static String wordWrap(String text, int maxColumnWidth, String lineSeparator) { + if (maxColumnWidth <= 0) throw new IllegalArgumentException("maxColumnWidth must be > 0"); + + String buffer = ""; + String lineBuffer = ""; + String[] words = WHITESPACE_PATTERN.split(text); + for (int i = 0; i < words.length; i++) { + if (words[i].length() + 1 + lineBuffer.length() <= maxColumnWidth) { + if (lineBuffer.length() != 0) lineBuffer += " "; + lineBuffer += words[i]; + } else if (words[i].length() <= maxColumnWidth) { + buffer += lineBuffer.trim() + lineSeparator; + lineBuffer = ""; + i--; + } else { + buffer += lineBuffer.trim() + lineSeparator; + lineBuffer = ""; + for (int j = 0; j < words[i].length(); j += maxColumnWidth) { + lineBuffer = words[i].substring(j, Math.min(j + maxColumnWidth, + words[i].length())); + if (lineBuffer.length() == maxColumnWidth) { + buffer += lineBuffer.trim() + lineSeparator; + lineBuffer = ""; + } + } + } + } + if (lineBuffer.length() != 0) buffer += lineBuffer.trim(); + return buffer.trim(); + } +} diff --git a/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/Version.java b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/Version.java new file mode 100644 index 0000000..087746e --- /dev/null +++ b/ch.psi.imagej.cbf/src/main/java/imcacat/jcbf/Version.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +/** + * I provide the version identifier for this library. + */ +/* + * WARNING: Changes made by hand to the value of my ID field will be lost; + * it is automatically set at build time. + */ +public class Version { + public static final String ID = "1.1.0"; +} diff --git a/ch.psi.imagej.cbf/src/main/resources/.gitignore b/ch.psi.imagej.cbf/src/main/resources/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/Base64Test.java b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/Base64Test.java new file mode 100644 index 0000000..fb06067 --- /dev/null +++ b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/Base64Test.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * I test Base64. + */ +public class Base64Test extends TestUtils { + @Test + public void testEncode01() { + assertEquals("YWRtaW46YWRtaW4=", Base64.encode(asciiBytes("admin:admin"))); + assertEquals("YWRtaW46YWRtaQ==", Base64.encode(asciiBytes("admin:admi"))); + assertEquals("YWRtaW46YWRt", Base64.encode(asciiBytes("admin:adm"))); + assertEquals("", Base64.encode(asciiBytes(""))); + assertEquals("YQ==", Base64.encode(asciiBytes("a"))); + assertEquals("YWI=", Base64.encode(asciiBytes("ab"))); + assertEquals("YWJj", Base64.encode(asciiBytes("abc"))); + assertEquals("QWxhZGRpbjpvcGVuIHNlc2FtZQ==", Base64.encode(asciiBytes("Aladdin:open sesame"))); + } + + /* Source data outside of US-ASCII range. */ + @Test + public void testEncode02() { + byte[] source = new byte[] { 0 }; + assertEquals("AA==", Base64.encode(source)); + source = new byte[] { -1 }; + assertEquals("/w==", Base64.encode(source)); + source = new byte[] { 1 }; + assertEquals("AQ==", Base64.encode(source)); + source = new byte[] { (byte)200, (byte)202, (byte)204, (byte)205, (byte)207 }; + assertArrayEquals(source, Base64.decode(Base64.encode(source))); + source = new byte[] { 0, -2, -4, -5, -7 }; + assertArrayEquals(source, Base64.decode(Base64.encode(source))); + source = new byte[] { Byte.MIN_VALUE, Byte.MAX_VALUE, -1 }; + assertArrayEquals(source, Base64.decode(Base64.encode(source))); + } + + @Test + public void testDecode01() { + assertArrayEquals(asciiBytes("admin:admin"), Base64.decode("YWRtaW46YWRtaW4=")); + assertArrayEquals(asciiBytes("admin:admi"), Base64.decode("YWRtaW46YWRtaQ==")); + assertArrayEquals(asciiBytes("admin:adm"), Base64.decode("YWRtaW46YWRt")); + assertArrayEquals(asciiBytes(""), Base64.decode("")); + assertArrayEquals(asciiBytes("a"), Base64.decode("YQ==")); + assertArrayEquals(asciiBytes("ab"), Base64.decode("YWI=")); + assertArrayEquals(asciiBytes("abc"), Base64.decode("YWJj")); + assertArrayEquals(asciiBytes("Aladdin:open sesame"), + Base64.decode("QWxhZGRpbjpvcGVuIHNlc2FtZQ==")); + } + + /* Invalid character. */ + @Test(expected = IllegalArgumentException.class) + public void testDecode02() { + Base64.decode("Y;RtaW46YWRtaW4="); + } + + /* Padding character is invalid when not at end. */ + @Test(expected = IllegalArgumentException.class) + public void testDecode03() { + Base64.decode("YW=taW46YWRtaW4="); + } + + /* Invalid length. */ + @Test(expected = IllegalArgumentException.class) + public void testDecode04() { + Base64.decode("YRtaW46YWRtaW4="); + } +} diff --git a/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfCompressionTest.java b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfCompressionTest.java new file mode 100644 index 0000000..6ead94f --- /dev/null +++ b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfCompressionTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import static org.junit.Assert.assertEquals; + +import java.text.ParseException; + +import org.junit.Test; + +/** + * I test CbfCompression. + */ +public class CbfCompressionTest extends TestUtils { + @Test + public void testParse01() throws ParseException { + assertEquals(CbfCompression.NONE, CbfCompression.parse("x-CBF_NONE")); + assertEquals(CbfCompression.BYTE_OFFSET, CbfCompression.parse("x-CBF_BYTE_OFFSET")); + } +} diff --git a/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfElementTypeTest.java b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfElementTypeTest.java new file mode 100644 index 0000000..9bfa3c5 --- /dev/null +++ b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfElementTypeTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import static org.junit.Assert.assertEquals; + +import java.text.ParseException; + +import org.junit.Test; + +/** + * I test CbfElementType. + */ +public class CbfElementTypeTest extends TestUtils { + @Test + public void testParse01() throws ParseException { + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, + CbfElementType.parse("signed 32-bit integer")); + } +} diff --git a/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageReaderSpiTest.java b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageReaderSpiTest.java new file mode 100644 index 0000000..ec67eed --- /dev/null +++ b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageReaderSpiTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import javax.imageio.stream.ImageInputStream; + +import org.junit.Test; + +/** + * I test CbfImageReaderSpi. + */ +public class CbfImageReaderSpiTest extends TestUtils { + @Test + public void testCanDecodeInput01() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + String input = "###CBF: VERSION"; + ImageInputStream stream = streamFromString(input); + assertTrue(spi.canDecodeInput(stream)); + } + + @Test + public void testCanDecodeInput02() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + String input = "###CBF: VERSION 1.5, CBFlib v0.7.8 - SLS/DECTRIS PILATUS detectors"; + ImageInputStream stream = streamFromString(input); + assertTrue(spi.canDecodeInput(stream)); + } + + @Test + public void testCanDecodeInput03() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + String input = + "###CBF: VERSION 1.5, CBFlib v0.7.8 - SLS/DECTRIS PILATUS detectors\n" + + "\n" + + "data_thaumatin_0001\n"; + ImageInputStream stream = streamFromString(input); + assertTrue(spi.canDecodeInput(stream)); + } + + @Test + public void testCanDecodeInput04() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + String input = + "###CBF: VERSIO"; + ImageInputStream stream = streamFromString(input); + assertFalse(spi.canDecodeInput(stream)); + } + + @Test + public void testCanDecodeInput05() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + String input = "###CBF: VERSION"; + assertFalse(spi.canDecodeInput(input)); + } +} diff --git a/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageReaderTest.java b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageReaderTest.java new file mode 100644 index 0000000..953a3e9 --- /dev/null +++ b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageReaderTest.java @@ -0,0 +1,953 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; +import java.util.Locale; + +import javax.imageio.IIOException; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriteParam; +import javax.imageio.event.IIOReadProgressListener; +import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.stream.ImageInputStream; + +import org.junit.Test; + +/** + * I test CbfImageReader. + */ +public class CbfImageReaderTest extends TestUtils { + /* Ensure OK to invoke getImageMetadata twice. */ + @Test + public void testGetImageMetadata01() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + ImageInputStream stream = streamFromBytes(createImageWithoutData()); + reader.setInput(stream); + reader.getImageMetadata(0); + reader.getImageMetadata(0); + } + + /* Ensure state is reset if new input source is set. */ + @Test + public void testSetInput01() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + ImageInputStream stream = streamFromBytes(createImageWithoutData()); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals("thaumatin_0001", mdata.getDataBlockName()); + String text = createImageText().replaceFirst("data_thaumatin_0001", "data_thaumatin_0002"); + stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals("thaumatin_0002", mdata.getDataBlockName()); + } + + @Test + public void testReadMetadataIdentifier01() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + ImageInputStream stream = streamFromBytes(createImageWithoutData()); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + String expected = "###CBF: VERSION 1.5, CBFlib v0.7.8 - SLS/DECTRIS PILATUS detectors"; + assertEquals(expected, mdata.getIdentifier()); + } + + @Test + public void testReadMetadataIdentifier02() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + "###CBF: VERSION 1.5, CBFlib v0.7.8 - SLS/DECTRIS PILATUS detectors", + "###CBF: VERSION 1.5\r\n# CBF file written by CBFlib v0.7.8"); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + String expected = "###CBF: VERSION 1.5"; + assertEquals(expected, mdata.getIdentifier()); + } + + @Test + public void testReadMetadataDataBlockName01() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + ImageInputStream stream = streamFromBytes(createImageWithoutData()); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + String expected = "thaumatin_0001"; + assertEquals(expected, mdata.getDataBlockName()); + } + + @Test + public void testReadMetadataHeaderConvention01() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + ImageInputStream stream = streamFromBytes(createImageWithoutData()); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + String expected = "SLS_1.0"; + assertEquals(expected, mdata.getHeaderConvention()); + } + + @Test + public void testReadMetadataHeaderConvention02() throws IOException, InterruptedException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + "_array_data\\.header_convention \"SLS_1\\.0\"", + "_array_data.header_convention \"SLS_1.1\""); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + final List warnings = new ArrayList(); + reader.addIIOReadWarningListener(new IIOReadWarningListener() { + @Override + public void warningOccurred(ImageReader source, String warning) { + warnings.add(warning); + } + }); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals("SLS_1.1", mdata.getHeaderConvention()); + assertEquals(1, warnings.size()); + assertEquals("Unrecognized header convention: SLS_1.1", warnings.get(0)); + } + + /* Header convention without quotes. */ + @Test + public void testReadMetadataHeaderConvention03() throws IOException, InterruptedException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + "_array_data\\.header_convention \"SLS_1\\.0\"", + "_array_data.header_convention SLS_1.0"); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals("SLS_1.0", mdata.getHeaderConvention()); + } + + /* Header convention not starting with "SLS_1.". */ + @Test(expected = IIOException.class) + public void testReadMetadataHeaderConvention04() throws IOException, InterruptedException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + "_array_data\\.header_convention \"SLS_1\\.0\"", + "_array_data.header_convention \"SLS_2.0\""); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + reader.getImageMetadata(0); + } + + @Test + public void testReadMetadataHeader01() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + ImageInputStream stream = streamFromBytes(createImageWithoutData()); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals("PILATUS 6M, 60-0103, IMCA-CAT", mdata.getHeaderDetector()); + Calendar c = Calendar.getInstance(Locale.US); + c.set(Calendar.YEAR, 2010); + c.set(Calendar.MONTH, 10); + c.set(Calendar.DAY_OF_MONTH, 17); + c.set(Calendar.HOUR_OF_DAY, 15); + c.set(Calendar.MINUTE, 52); + c.set(Calendar.SECOND, 30); + c.set(Calendar.MILLISECOND, 834); + assertEquals(c.getTime(), mdata.getHeaderDate()); + assertArrayEquals(new float[] { 172e-6f, 172e-6f }, mdata.getHeaderPixelSize(), 0.0f); + assertEquals("Silicon", mdata.getHeaderSensorType()); + assertEquals((Float)0.000320f, mdata.getHeaderSensorThickness()); + assertEquals((Float)0.081033f, mdata.getHeaderExposureTime()); + assertEquals((Float)0.083333f, mdata.getHeaderExposurePeriod()); + assertEquals((Float)124.0e-09f, mdata.getHeaderTau()); + assertEquals((Integer)326741, mdata.getHeaderCountCutoff()); + assertEquals((Float)7439.0f, mdata.getHeaderThreshold()); + assertNull(mdata.getHeaderGainType()); + assertEquals((Float)9.900f, mdata.getHeaderGainVrf()); + assertEquals((Integer)2391, mdata.getHeaderNumExcludedPixels()); + assertEquals("badpix_mask.tif", mdata.getHeaderExcludedPixels()); + assertNull(mdata.getHeaderFlatField()); + assertNull(mdata.getHeaderTrim()); + assertEquals((Float)1.0000f, mdata.getHeaderWavelength()); + assertArrayEquals(new float[] { 0.0f, 0.0f }, mdata.getHeaderEnergyRange(), 0.0f); + assertEquals((Float)0.94997f, mdata.getHeaderDetectorDistance()); + assertEquals((Float)0.0f, mdata.getHeaderDetectorVOffset()); + assertArrayEquals(new float[] { 1253.0f, 1288.0f }, mdata.getHeaderBeamXY(), 0.0f); + assertEquals((Float)0.0f, mdata.getHeaderFlux()); + assertEquals((Float)0.0f, mdata.getHeaderFilterTransmission()); + assertEquals((Float)0.00f, mdata.getHeaderStartAngle()); + assertEquals((Float)1.0f, mdata.getHeaderAngleIncrement()); + assertEquals((Float)0.0007f, mdata.getHeaderDetector2Theta()); + assertEquals((Float)0.99f, mdata.getHeaderPolarization()); + assertEquals((Float)0.0f, mdata.getHeaderAlpha()); + assertEquals((Float)0.0f, mdata.getHeaderKappa()); + assertEquals((Float)0.0f, mdata.getHeaderPhi()); + assertEquals((Float)0.0f, mdata.getHeaderChi()); + assertEquals("X, CW", mdata.getHeaderOscillationAxis()); + assertEquals((Integer)1, mdata.getHeaderNumOscillations()); + } + + /* Two date formats. */ + @Test + public void testReadMetadataHeader02() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + ImageInputStream stream = streamFromBytes(createImageWithoutData()); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + Calendar c = Calendar.getInstance(Locale.US); + c.set(Calendar.YEAR, 2010); + c.set(Calendar.MONTH, 10); + c.set(Calendar.DAY_OF_MONTH, 17); + c.set(Calendar.HOUR_OF_DAY, 15); + c.set(Calendar.MINUTE, 52); + c.set(Calendar.SECOND, 30); + c.set(Calendar.MILLISECOND, 834); + assertEquals(c.getTime(), mdata.getHeaderDate()); + String text = createImageText().replaceFirst( + "# 2010-Nov-17T15:52:30.834\r\n", + "# 2010/Nov/17 15:52:30.834\r\n"); + stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(c.getTime(), mdata.getHeaderDate()); + } + + /* "(nil)" excluded pixels and trim directory; not "(nil)" flat field. */ + @Test + public void testReadMetadataHeader03() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + "# Excluded_pixels: badpix_mask.tif\r\n", + "# Excluded_pixels: (nil)\r\n"); + text = text.replaceFirst( + "# Flat_field: \\(nil\\)\r\n", + "# Flat_field: flat_field.tif\r\n"); + text = text.replaceFirst( + "Trim_directory: \r\n", + "Trim_directory: (nil)\r\n"); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertNull(mdata.getHeaderExcludedPixels()); + assertEquals("flat_field.tif", mdata.getHeaderFlatField()); + assertNull(mdata.getHeaderTrim()); + } + + /* not "(nil)" trim directory. */ + @Test + public void testReadMetadataHeader04() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + "Trim_directory: \r\n", + "Trim_directory: p6m0103_T8p0_vrf_m0p3_100323\r\n"); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals("p6m0103_T8p0_vrf_m0p3_100323", mdata.getHeaderTrim()); + } + + @Test + public void testReadMetadataMimeHeader01() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + ImageInputStream stream = streamFromBytes(createImageWithoutData()); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(CbfCompression.BYTE_OFFSET, mdata.getDataCompression()); + assertEquals(6224001, mdata.getDataSize()); + assertEquals(1, mdata.getDataId()); + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, mdata.getDataElementType()); + assertEquals(ByteOrder.LITTLE_ENDIAN, mdata.getDataElementByteOrder()); + assertArrayEquals(Base64.decode("Ti67wd6I6DPuGIsOnMalMw=="), mdata.getDataMd5()); + assertEquals(6224001, mdata.getDataNumElements()); + assertEquals(2463, mdata.getDataWidth()); + assertEquals(2527, mdata.getDataHeight()); + assertEquals(4095, mdata.getDataPadding()); + } + + @Test + public void testReadMetadataMimeHeader02() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + " conversions=\"x-CBF_BYTE_OFFSET\"", + " conversions=\"x-CBF_NONE\""); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(CbfCompression.NONE, mdata.getDataCompression()); + } + + /* Missing content type conversions. */ + @Test(expected = IIOException.class) + public void testReadMetadataMimeHeader03() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText(); + text = text.replaceFirst( + "Content-Type: application/octet-stream;", + "Content-Type: application/octet-stream"); + text = text.replaceFirst( + " +conversions=\"x-CBF_BYTE_OFFSET\"\r\n", + ""); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + reader.getImageMetadata(0); + } + + @Test + public void testReadMetadataMimeHeader04() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + "X-Binary-Element-Type: \"signed 32-bit integer\"", + "X-Binary-Element-Type: \"unsigned 32-bit integer\""); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(CbfElementType.UNSIGNED_32_BIT_INTEGER, mdata.getDataElementType()); + } + + @Test + public void testReadMetadataMimeHeader05() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + "X-Binary-Element-Byte-Order: LITTLE_ENDIAN", + "X-Binary-Element-Byte-Order: BIG_ENDIAN"); + ImageInputStream stream = streamFromBytes(createImageWithoutData(asciiBytes(text))); + reader.setInput(stream); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(ByteOrder.BIG_ENDIAN, mdata.getDataElementByteOrder()); + } + + /* Element type: unsigned 8-bit integer; compression: none. */ + @Test + public void testRead01() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + String text = createImageText().replaceFirst( + "conversions=\"x-CBF_BYTE_OFFSET\"", + "conversions=\"x-CBF_NONE\""); + text = text.replaceFirst( + "X-Binary-Size: 6224001", + "X-Binary-Size: 4"); + text = text.replaceFirst( + "X-Binary-Element-Type: \"signed 32-bit integer\"", + "X-Binary-Element-Type: \"unsigned 8-bit integer\""); + text = text.replaceFirst( + "Content-MD5: Ti67wd6I6DPuGIsOnMalMw==\r\n", + ""); + text = text.replaceFirst( + "X-Binary-Number-of-Elements: 6224001", + "X-Binary-Number-of-Elements: 4"); + text = text.replaceFirst( + "X-Binary-Size-Fastest-Dimension: 2463", + "X-Binary-Size-Fastest-Dimension: 2"); + text = text.replaceFirst( + "X-Binary-Size-Second-Dimension: 2527", + "X-Binary-Size-Second-Dimension: 2"); + byte[] imageWithoutData = createImageWithoutData(asciiBytes(text)); + byte[] imageData = { 1, 2, 3, 4 }; + byte[] imageWithData = new byte[imageWithoutData.length + imageData.length]; + System.arraycopy(imageWithoutData, 0, imageWithData, 0, imageWithoutData.length); + System.arraycopy(imageData, 0, imageWithData, imageWithoutData.length, imageData.length); + ImageInputStream stream = streamFromBytes(imageWithData); + reader.setInput(stream); + BufferedImage image = reader.read(0); + Raster raster = image.getData(); + assertEquals(1, raster.getNumBands()); + assertEquals(8, image.getColorModel().getComponentSize(0)); + assertEquals(2, raster.getWidth()); + assertEquals(2, raster.getHeight()); + assertEquals(BufferedImage.TYPE_BYTE_GRAY, image.getType()); + assertEquals(imageData[0], (byte)raster.getSample(0, 0, 0)); + assertEquals(imageData[1], (byte)raster.getSample(1, 0, 0)); + assertEquals(imageData[2], (byte)raster.getSample(0, 1, 0)); + assertEquals(imageData[3], (byte)raster.getSample(1, 1, 0)); + } + + /* Element type: signed 32-bit integer; compression: byte_offset. */ + @Test + public void testRead02() throws IOException { + int[][] expected = { + { +1, +2 }, + { -1, -2 } + }; + BufferedImage outImage = createIntBufferedImage(expected); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Element type: signed 32-bit integer; compression: byte_offset. Min and + * max values. + */ + @Test + public void testRead03() throws IOException { + int[][] expected = { + { 0, Integer.MAX_VALUE }, + { 5, Integer.MIN_VALUE } + }; + BufferedImage outImage = createIntBufferedImage(expected); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Not square dimensions. */ + @Test + public void testRead04() throws IOException { + int[][] expected = { + { 0, 1, 2 }, + { 3, 4, 5 } + }; + BufferedImage outImage = createIntBufferedImage(expected); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Element type: signed 32-bit integer; compression: byte_offset. Short + * size values. + */ + @Test + public void testRead05() throws IOException { + int[][] expected = { + { Short.MIN_VALUE + 1, Short.MAX_VALUE }, + { 5, 6 } + }; + BufferedImage outImage = createIntBufferedImage(expected); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region. */ + @Test + public void testRead06() throws IOException { + int[][] output = { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + int[][] expected = { + { 1, 2 }, + { 4, 5 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(0, 0, 2, 2)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region. */ + @Test + public void testRead07() throws IOException { + int[][] output = { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + int[][] expected = { + { 2, 3 }, + { 5, 6 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(1, 0, 2, 2)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region. */ + @Test + public void testRead08() throws IOException { + int[][] output = { + { 1, 2, 3 }, + { 4, 5, 6 }, + { 7, 8, 9 } + }; + int[][] expected = { + { 4, 5 }, + { 7, 8 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(0, 1, 2, 2)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region. */ + @Test + public void testRead09() throws IOException { + int[][] output = { + { 1, 2, 3 }, + { 4, 5, 6 }, + { 7, 8, 9 } + }; + int[][] expected = { + { 5, 6 }, + { 8, 9 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(1, 1, 2, 2)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region. */ + @Test + public void testRead10() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 15, 16 }, + { 19, 20 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(1, 1, 2, 2)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region. */ + @Test + public void testRead11() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 10, 11, 12, 13 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(0, 0, 4, 1)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region. */ + @Test + public void testRead12() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 15, 16, 17 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(1, 1, 3, 1)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region. */ + @Test + public void testRead13() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 10 }, + { 14 }, + { 18 }, + { 22 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(0, 0, 1, 4)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region. */ + @Test + public void testRead14() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 15 }, + { 19 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(1, 1, 1, 2)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region of size 1x1. */ + @Test + public void testRead15() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 25 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(3, 3, 1, 1)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* 2x2 subsampling without offset. */ + @Test + public void testRead16() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 10, 12 }, + { 18, 20 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceSubsampling(2, 2, 0, 0); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* 2x1 subsampling without offset. */ + @Test + public void testRead17() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 10, 12 }, + { 14, 16 }, + { 18, 20 }, + { 22, 24 }, + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceSubsampling(2, 1, 0, 0); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* 1x2 subsampling without offset. */ + @Test + public void testRead18() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 10, 11, 12, 13 }, + { 18, 19, 20, 21 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceSubsampling(1, 2, 0, 0); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* 3x3 subsampling without offset. */ + @Test + public void testRead19() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 10, 13 }, + { 22, 25 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceSubsampling(3, 3, 0, 0); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* 2x2 subsampling with offset. */ + @Test + public void testRead20() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 15, 17 }, + { 23, 25 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceSubsampling(2, 2, 1, 1); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* 2x1 subsampling with X offset. */ + @Test + public void testRead21() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 11, 13 }, + { 15, 17 }, + { 19, 21 }, + { 23, 25 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceSubsampling(2, 1, 1, 0); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* 1x2 subsampling with Y offset. */ + @Test + public void testRead22() throws IOException { + int[][] output = { + { 10, 11, 12, 13 }, + { 14, 15, 16, 17 }, + { 18, 19, 20, 21 }, + { 22, 23, 24, 25 } + }; + int[][] expected = { + { 14, 15, 16, 17 }, + { 22, 23, 24, 25 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceSubsampling(1, 2, 0, 1); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* 3x2 subsampling with offset. */ + @Test + public void testRead23() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 } + }; + int[][] expected = { + { 18, 21 }, + { 30, 33 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceSubsampling(3, 2, 2, 1); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region and 2x2 subsampling. */ + @Test + public void testRead24() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 17, 19 }, + { 29, 31 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(1, 1, 4, 4)); + param.setSourceSubsampling(2, 2, 0, 0); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region and 2x2 subsampling with offset. */ + @Test + public void testRead25() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 24, 26 }, + { 36, 38 } + }; + BufferedImage outImage = createIntBufferedImage(output); + ImageReadParam param = createImageReadParam(); + param.setSourceRegion(new Rectangle(1, 1, 4, 4)); + param.setSourceSubsampling(2, 2, 1, 1); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage), param); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Progress. */ + @Test + public void testRead26() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + BufferedImage outImage = createIntBufferedImage(output); + final List started = Arrays.asList(new Boolean[] { Boolean.FALSE }); + final List percentDone = new ArrayList(); + final List completed = Arrays.asList(new Boolean[] { Boolean.FALSE }); + final List aborted = Arrays.asList(new Boolean[] { Boolean.FALSE }); + IIOReadProgressListener listener = new IIOReadProgressListener() { + public void sequenceStarted(ImageReader source, int minIndex) { + } + + public void sequenceComplete(ImageReader source) { + } + + public void imageStarted(ImageReader source, int imageIndex) { + started.set(0, Boolean.TRUE); + } + + public void imageProgress(ImageReader source, float percentageDone) { + percentDone.add(percentageDone); + } + + public void imageComplete(ImageReader source) { + completed.set(0, Boolean.TRUE); + } + + public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) { + } + + public void thumbnailProgress(ImageReader source, float percentageDone) { + } + + public void thumbnailComplete(ImageReader source) { + } + + public void readAborted(ImageReader source) { + aborted.set(0, Boolean.TRUE); + } + }; + readBufferedImage(writeBufferedImage(outImage), listener); + assertTrue(started.get(0)); + assertTrue(completed.get(0)); + assertFalse(aborted.get(0)); + assertEquals(5, percentDone.size()); + assertEquals(80.0f, percentDone.get(percentDone.size() - 1), 0.1f); + } + + /* + * Read into second band (index 1) of RGB destination image. + */ + @Test + public void testRead27() throws IOException { + int[][][] output = { + { { 0, 0, 0 }, { 0, +1, 0 }, { 0, +2, 0 } }, + { { 0, 3, 0 }, { 0, +4, 0 }, { 0, +5, 0 } }, + { { 0, 6, 0 }, { 0, +7, 0 }, { 0, +8, 0 } }, + { { 0, 9, 0 }, { 0, 10, 0 }, { 0, 11, 0 } } + }; + int[][][] expected = output; + + BufferedImage outImage = createIntRgbBufferedImage(output); + ImageWriteParam outParam = createImageWriteParam(); + outParam.setSourceBands(new int[] { 1 }); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + byte[] outBytes = writeBufferedImage(outImage, outMdata, outParam); + BufferedImage inImage = createIntRgbBufferedImage(expected[0].length, expected.length); + ImageReadParam inParam = createImageReadParam(); + inParam.setDestination(inImage); + inParam.setDestinationBands(new int[] { 1 }); + BufferedImage inImage2 = readBufferedImage(outBytes, inParam); + assertSame(inImage, inImage2); + assertArrayEquals(expected, getIntRgbPixels(inImage)); + } +} diff --git a/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageWriterSpiTest.java b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageWriterSpiTest.java new file mode 100644 index 0000000..e142ebe --- /dev/null +++ b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageWriterSpiTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * I test CbfImageWriterSpi. + */ +public class CbfImageWriterSpiTest extends TestUtils { + @Test + public void testCanEncodeImage01() { + CbfImageWriterSpi spi = new CbfImageWriterSpi(); + assertTrue(spi.canEncodeImage(createByteBinarySpecifier())); + assertTrue(spi.canEncodeImage(createUbyteSpecifier())); + assertTrue(spi.canEncodeImage(createByteSpecifier())); + assertTrue(spi.canEncodeImage(createUshortSpecifier())); + assertTrue(spi.canEncodeImage(createShortSpecifier())); + assertTrue(spi.canEncodeImage(createIntSpecifier())); + assertTrue(spi.canEncodeImage(createFloatSpecifier())); + assertTrue(spi.canEncodeImage(createDoubleSpecifier())); + } +} diff --git a/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageWriterTest.java b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageWriterTest.java new file mode 100644 index 0000000..05d48f3 --- /dev/null +++ b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfImageWriterTest.java @@ -0,0 +1,1731 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import javax.imageio.IIOImage; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.event.IIOWriteProgressListener; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.MemoryCacheImageOutputStream; + +import org.junit.Test; + +/** + * I test CbfImageWriter. + */ +public class CbfImageWriterTest extends TestUtils { + @Test + public void testWriteHeader01() throws IOException { + ByteArrayOutputStream baStream = new ByteArrayOutputStream(); + ImageOutputStream stream = createOutputStream(baStream); + CbfMetadata mdata = createMetadata(); + CbfImageWriter writer = createWriter(stream); + writer.write(null, create1x1Image(mdata), null); + String expected = "# Detector: PILATUS 6M\r\n" + + "# 2010-Nov-17T15:52:30.834\r\n" + + "# Pixel_size 172e-6 m x 172e-6 m\r\n" + + "# Silicon sensor, thickness 0.000320 m\r\n" + + "# Exposure_time 1.000000 s\r\n" + + "# Exposure_period 1.000000 s\r\n" + + "# Tau = 124.0e-09 s\r\n" + + "# Count_cutoff 1048575 counts\r\n" + + "# Threshold_setting 7439 eV\r\n" + + "# Gain_setting not implemented (vrf = 9.900)\r\n" + + "# Wavelength 1.0000 A\r\n" + + "# Energy_range (0, 0) eV\r\n" + + "# Detector_distance 0.20000 m\r\n" + + "# Detector_Voffset 0.00000 m\r\n" + + "# Beam_xy (1231.50, 1263.50) pixels\r\n" + + "# Flux 0.0000 ph/s\r\n" + + "# Filter_transmission 0.0000\r\n" + + "# Start_angle 0.0000 deg.\r\n" + + "# Angle_increment 0.0000 deg.\r\n" + + "# Detector_2theta 0.0000 deg.\r\n" + + "# Polarization 0.990\r\n" + + "# Alpha 0.0000 deg.\r\n" + + "# Kappa 0.0000 deg.\r\n" + + "# Phi 0.0000 deg.\r\n" + + "# Chi 0.0000 deg.\r\n" + + "# Oscillation_axis X, CW\r\n" + + "# N_oscillations 1\r\n"; + String actual = extractHeader(baStream.toByteArray()); + assertEquals(expected, actual); + } + + /* No gain type. */ + @Test + public void testWriteHeader02() throws IOException { + ByteArrayOutputStream baStream = new ByteArrayOutputStream(); + ImageOutputStream stream = createOutputStream(baStream); + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderGainVrf(9.9f); + CbfImageWriter writer = createWriter(stream); + writer.write(null, create1x1Image(mdata), null); + String expected = "# Gain_setting not implemented (vrf = 9.900)\r\n"; + String actual = extractHeader(baStream.toByteArray()); + assertEquals(expected, actual); + } + + /* Excluded pixels. */ + @Test + public void testWriteHeader03() throws IOException { + ByteArrayOutputStream baStream = new ByteArrayOutputStream(); + ImageOutputStream stream = createOutputStream(baStream); + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderExcludedPixels("badpix_mask.tif"); + CbfImageWriter writer = createWriter(stream); + writer.write(null, create1x1Image(mdata), null); + String expected = "# Excluded_pixels: badpix_mask.tif\r\n"; + String actual = extractHeader(baStream.toByteArray()); + assertEquals(expected, actual); + } + + /* Flat field. */ + @Test + public void testWriteHeader04() throws IOException { + ByteArrayOutputStream baStream = new ByteArrayOutputStream(); + ImageOutputStream stream = createOutputStream(baStream); + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderFlatField("flat_field.tif"); + CbfImageWriter writer = createWriter(stream); + writer.write(null, create1x1Image(mdata), null); + String expected = "# Flat_field: flat_field.tif\r\n"; + String actual = extractHeader(baStream.toByteArray()); + assertEquals(expected, actual); + } + + /* Trim directory. */ + @Test + public void testWriteHeader05() throws IOException { + ByteArrayOutputStream baStream = new ByteArrayOutputStream(); + ImageOutputStream stream = createOutputStream(baStream); + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderTrim("p6m0103_T8p0_vrf_m0p3_100323"); + CbfImageWriter writer = createWriter(stream); + writer.write(null, create1x1Image(mdata), null); + String expected = "# Trim_directory: p6m0103_T8p0_vrf_m0p3_100323\r\n"; + String actual = extractHeader(baStream.toByteArray()); + assertEquals(expected, actual); + } + + /* Unrecognized header fields. */ + @Test + public void testWriteHeader06() throws IOException { + ByteArrayOutputStream baStream = new ByteArrayOutputStream(); + ImageOutputStream stream = createOutputStream(baStream); + CbfMetadata mdata = new CbfMetadata(); + mdata.addHeaderUnrecognizedLine("# Favorite_color: Blue"); + mdata.addHeaderUnrecognizedLine("# Favorite_shape: Box"); + CbfImageWriter writer = createWriter(stream); + writer.write(null, create1x1Image(mdata), null); + String expected = "# Favorite_color: Blue\r\n# Favorite_shape: Box\r\n"; + String actual = extractHeader(baStream.toByteArray()); + assertEquals(expected, actual); + } + + /* Comment. */ + @Test + public void testWriteHeader07() throws IOException { + ByteArrayOutputStream baStream = new ByteArrayOutputStream(); + ImageOutputStream stream = createOutputStream(baStream); + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderComment("Best crystal ever!"); + CbfImageWriter writer = createWriter(stream); + writer.write(null, create1x1Image(mdata), null); + String expected = "# Comment: Best crystal ever!\r\n"; + String actual = extractHeader(baStream.toByteArray()); + assertEquals(expected, actual); + } + + /* + * Source region and 2x2 subsampling with offset with byte_offset + * compression. + */ + @Test + public void testWrite01() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 24, 26 }, + { 36, 38 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(1, 1, 4, 4)); + param.setSourceSubsampling(2, 2, 1, 1); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Source region and 3x1 subsampling with offset with byte_offset + * compression. + */ + @Test + public void testWrite02() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 18 }, + { 24 }, + { 30 }, + { 36 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(1, 1, 4, 4)); + param.setSourceSubsampling(3, 1, 1, 0); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region and 2x2 subsampling with byte_offset compression. */ + @Test + public void testWrite03() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 19, 21 }, + { 31, 33 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(3, 1, 3, 4)); + param.setSourceSubsampling(2, 2, 0, 0); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region and 3x2 subsampling with byte_offset compression. */ + @Test + public void testWrite04() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 23, 26 }, + { 35, 38 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(1, 2, 4, 3)); + param.setSourceSubsampling(3, 2, 0, 0); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region with byte_offset compression. */ + @Test + public void testWrite05() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 18, 19, 20 }, + { 24, 25, 26 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(2, 1, 3, 2)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region is entire image with byte_offset compression. */ + @Test + public void testWrite06() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(0, 0, output[0].length, output.length)); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Signed 32-bit integer with byte_offset compression. */ + @Test + public void testWrite07() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage)); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Binary image stored as 8-bit unsigned integer with byte_offset + * compression. + */ + @Test + public void testWrite08() throws IOException { + byte[][] output = { + { 0, 1, 1 }, + { 1, 0, 1 } + }; + byte[][] expected = output; + + BufferedImage outImage = createByteBinaryBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_8_BIT_INTEGER); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_BYTE_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_BYTE, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getBytePixels(inImage)); + } + + /* Unsigned 8-bit integer with byte_offset compression. */ + @Test + public void testWrite09() throws IOException { + short[][] output = { + { Byte.MAX_VALUE + 3, 6, 7 }, + { Byte.MAX_VALUE + 5, 8, 9 } + }; + short[][] expected = output; + + BufferedImage outImage = createUbyteBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_8_BIT_INTEGER); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_BYTE_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_BYTE, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getShortPixels(inImage)); + } + + /* Signed 8-bit integer with byte_offset compression. */ + @Test + public void testWrite10() throws IOException { + byte[][] output = { + { -1, -2, -3 }, + { +4, +5, +6 } + }; + byte[][] expected = output; + + BufferedImage outImage = createByteBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.SIGNED_8_BIT_INTEGER); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_SHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getBytePixels(inImage)); + } + + /* + * Signed 8-bit integer, without specified element type, with byte_offset + * compression. + */ + @Test + public void testWrite11() throws IOException { + byte[][] output = { + { -1, -2, -3 }, + { +4, +5, +6 } + }; + byte[][] expected = output; + + BufferedImage outImage = createByteBufferedImage(output); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_SHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getBytePixels(inImage)); + } + + /* Unsigned 16-bit integer with byte_offset compression. */ + @Test + public void testWrite12() throws IOException { + int[][] output = { + { Short.MAX_VALUE + 3, 6, 7 }, + { Short.MAX_VALUE + 5, 8, 9 } + }; + int[][] expected = output; + + BufferedImage outImage = createUshortBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_16_BIT_INTEGER); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_USHORT_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_USHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Signed 16-bit integer with byte_offset compression. */ + @Test + public void testWrite13() throws IOException { + short[][] output = { + { Short.MAX_VALUE, +1, +2, +3 }, + { Short.MIN_VALUE, -1, -2, -3 } + }; + short[][] expected = output; + + BufferedImage outImage = createShortBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.SIGNED_16_BIT_INTEGER); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_SHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getShortPixels(inImage)); + } + + /* + * Signed 16-bit integer, without specified element type, with byte_offset + * compression. + */ + @Test + public void testWrite14() throws IOException { + short[][] output = { + { Short.MAX_VALUE, +1, +2, +3 }, + { Short.MIN_VALUE, -1, -2, -3 } + }; + short[][] expected = output; + + BufferedImage outImage = createShortBufferedImage(output); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_SHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getShortPixels(inImage)); + } + + /* Float as unsigned 32-bit integer with byte_offset compression. */ + @Test + public void testWrite15() throws IOException { + float[][] output = { + { Integer.MAX_VALUE + 3.0f, 6.0f, Integer.MAX_VALUE + 50.0f }, + { Integer.MAX_VALUE + 5.0f, 8.0f, Integer.MAX_VALUE + 51.0f } + }; + float[][] expected = output; + + BufferedImage outImage = createFloatBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.UNSIGNED_32_BIT_INTEGER); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.UNSIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_FLOAT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* Float as signed 32-bit integer with byte_offset compression. */ + @Test + public void testWrite16() throws IOException { + float[][] output = { + { Integer.MAX_VALUE, -6.0f, Integer.MAX_VALUE - 50.0f }, + { Integer.MIN_VALUE, +8.0f, Integer.MAX_VALUE - 51.0f } + }; + float[][] expected = output; + + BufferedImage outImage = createFloatBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* Int as unsigned 32-bit integer with byte_offset compression. */ + @Test + public void testWrite17() throws IOException { + int[][] output = { + { Integer.MAX_VALUE, 6, 7 }, + { Integer.MAX_VALUE, 0, Integer.MAX_VALUE - 1 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.UNSIGNED_32_BIT_INTEGER); + byte[] data = writeBufferedImage(outImage, outMdata); + CbfImageReader reader = createReader(); + reader.setInput(createInputStream(data)); + ImageReadParam param = reader.getDefaultReadParam(); + boolean foundIntType = false; + for (Iterator i = reader.getImageTypes(0); i.hasNext();) { + ImageTypeSpecifier each = i.next(); + if (each.getSampleModel().getDataType() != DataBuffer.TYPE_INT) continue; + param.setDestinationType(each); + foundIntType = true; + } + assertTrue(foundIntType); + BufferedImage inImage = reader.read(0, param); + CbfMetadata inMdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(CbfElementType.UNSIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Int as signed 32-bit integer with byte_offset compression. */ + @Test + public void testWrite18() throws IOException { + int[][] output = { + { Integer.MAX_VALUE, 6, Integer.MAX_VALUE - 1 }, + { Integer.MIN_VALUE, 8, Integer.MIN_VALUE + 1 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Int as signed 32-bit integer, without specified element type, with + * byte_offset compression. + */ + @Test + public void testWrite19() throws IOException { + int[][] output = { + { Integer.MAX_VALUE, 6, Integer.MAX_VALUE - 1 }, + { Integer.MIN_VALUE, 8, Integer.MIN_VALUE + 1 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + Object[] inImageAndMetadata = readBufferedImageAndMetadata(writeBufferedImage(outImage)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Float as signed 32-bit integer with byte_offset compression. */ + @Test + public void testWrite20() throws IOException { + float[][] output = { + { Integer.MAX_VALUE, 6, 7 }, + { Integer.MAX_VALUE, 0, Integer.MAX_VALUE - 1 } + }; + float[][] expected = output; + + BufferedImage outImage = createFloatBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + byte[] data = writeBufferedImage(outImage, outMdata); + CbfImageReader reader = createReader(); + reader.setInput(createInputStream(data)); + ImageReadParam param = reader.getDefaultReadParam(); + boolean foundFloatType = false; + for (Iterator i = reader.getImageTypes(0); i.hasNext();) { + ImageTypeSpecifier each = i.next(); + if (each.getSampleModel().getDataType() != DataBuffer.TYPE_FLOAT) continue; + param.setDestinationType(each); + foundFloatType = true; + } + assertTrue(foundFloatType); + BufferedImage inImage = reader.read(0, param); + CbfMetadata inMdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_FLOAT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* Source region and 2x2 subsampling with offset with none compression. */ + @Test + public void testWrite21() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 24, 26 }, + { 36, 38 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(1, 1, 4, 4)); + param.setSourceSubsampling(2, 2, 1, 1); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region and 3x1 subsampling with offset with none compression. */ + @Test + public void testWrite22() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 18 }, + { 24 }, + { 30 }, + { 36 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(1, 1, 4, 4)); + param.setSourceSubsampling(3, 1, 1, 0); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region and 2x2 subsampling with none compression. */ + @Test + public void testWrite23() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 19, 21 }, + { 31, 33 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(3, 1, 3, 4)); + param.setSourceSubsampling(2, 2, 0, 0); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region and 3x2 subsampling with none compression. */ + @Test + public void testWrite24() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 23, 26 }, + { 35, 38 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(1, 2, 4, 3)); + param.setSourceSubsampling(3, 2, 0, 0); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region with none compression. */ + @Test + public void testWrite25() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = { + { 18, 19, 20 }, + { 24, 25, 26 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(2, 1, 3, 2)); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Source region is entire image with none compression. */ + @Test + public void testWrite26() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setSourceRegion(new Rectangle(0, 0, output[0].length, output.length)); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Signed 32-bit integer with none compression. */ + @Test + public void testWrite27() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Binary image stored as 8-bit unsigned integer with none compression. */ + @Test + public void testWrite28() throws IOException { + byte[][] output = { + { 0, 1, 1 }, + { 1, 0, 1 } + }; + byte[][] expected = output; + + BufferedImage outImage = createByteBinaryBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_8_BIT_INTEGER); + mdata.setDataCompression(CbfCompression.NONE); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_BYTE_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_BYTE, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getBytePixels(inImage)); + } + + /* Unsigned 8-bit integer with none compression. */ + @Test + public void testWrite29() throws IOException { + short[][] output = { + { Byte.MAX_VALUE + 3, 6, 7 }, + { Byte.MAX_VALUE + 5, 8, 9 } + }; + short[][] expected = output; + + BufferedImage outImage = createUbyteBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_8_BIT_INTEGER); + mdata.setDataCompression(CbfCompression.NONE); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_BYTE_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_BYTE, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getShortPixels(inImage)); + } + + /* Signed 8-bit integer with none compression. */ + @Test + public void testWrite30() throws IOException { + byte[][] output = { + { -1, -2, -3 }, + { +4, +5, +6 } + }; + byte[][] expected = output; + + BufferedImage outImage = createByteBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.SIGNED_8_BIT_INTEGER); + mdata.setDataCompression(CbfCompression.NONE); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_SHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getBytePixels(inImage)); + } + + /* + * Signed 8-bit integer, without specified element type, with none + * compression. + */ + @Test + public void testWrite31() throws IOException { + byte[][] output = { + { -1, -2, -3 }, + { +4, +5, +6 } + }; + byte[][] expected = output; + + BufferedImage outImage = createByteBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_SHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getBytePixels(inImage)); + } + + /* Unsigned 16-bit integer with none compression. */ + @Test + public void testWrite32() throws IOException { + int[][] output = { + { Short.MAX_VALUE + 3, 6, 7 }, + { Short.MAX_VALUE + 5, 8, 9 } + }; + int[][] expected = output; + + BufferedImage outImage = createUshortBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_16_BIT_INTEGER); + mdata.setDataCompression(CbfCompression.NONE); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_USHORT_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_USHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Signed 16-bit integer with none compression. */ + @Test + public void testWrite33() throws IOException { + short[][] output = { + { Short.MAX_VALUE, +1, +2, +3 }, + { Short.MIN_VALUE, -1, -2, -3 } + }; + short[][] expected = output; + + BufferedImage outImage = createShortBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.SIGNED_16_BIT_INTEGER); + mdata.setDataCompression(CbfCompression.NONE); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_SHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getShortPixels(inImage)); + } + + /* + * Signed 16-bit integer, without specified element type, with none + * compression. + */ + @Test + public void testWrite34() throws IOException { + short[][] output = { + { Short.MAX_VALUE, +1, +2, +3 }, + { Short.MIN_VALUE, -1, -2, -3 } + }; + short[][] expected = output; + + BufferedImage outImage = createShortBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, param)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_SHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getShortPixels(inImage)); + } + + /* Float as unsigned 32-bit integer with none compression. */ + @Test + public void testWrite35() throws IOException { + float[][] output = { + { Integer.MAX_VALUE + 3.0f, 6.0f, Integer.MAX_VALUE + 50.0f }, + { Integer.MAX_VALUE + 5.0f, 8.0f, Integer.MAX_VALUE + 51.0f } + }; + float[][] expected = output; + + BufferedImage outImage = createFloatBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.UNSIGNED_32_BIT_INTEGER); + outMdata.setDataCompression(CbfCompression.NONE); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.UNSIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_FLOAT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* Float as signed 32-bit integer with none compression. */ + @Test + public void testWrite36() throws IOException { + float[][] output = { + { Integer.MAX_VALUE, -6.0f, Integer.MAX_VALUE - 50.0f }, + { Integer.MIN_VALUE, +8.0f, Integer.MAX_VALUE - 51.0f } + }; + float[][] expected = output; + + BufferedImage outImage = createFloatBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + outMdata.setDataCompression(CbfCompression.NONE); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* Int as unsigned 32-bit integer with none compression. */ + @Test + public void testWrite37() throws IOException { + int[][] output = { + { Integer.MAX_VALUE, 6, 7 }, + { Integer.MAX_VALUE, 0, Integer.MAX_VALUE - 1 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.UNSIGNED_32_BIT_INTEGER); + outMdata.setDataCompression(CbfCompression.NONE); + byte[] data = writeBufferedImage(outImage, outMdata); + CbfImageReader reader = createReader(); + reader.setInput(createInputStream(data)); + ImageReadParam param = reader.getDefaultReadParam(); + boolean foundIntType = false; + for (Iterator i = reader.getImageTypes(0); i.hasNext();) { + ImageTypeSpecifier each = i.next(); + if (each.getSampleModel().getDataType() != DataBuffer.TYPE_INT) continue; + param.setDestinationType(each); + foundIntType = true; + } + assertTrue(foundIntType); + BufferedImage inImage = reader.read(0, param); + CbfMetadata inMdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(CbfElementType.UNSIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Int as signed 32-bit integer with none compression. */ + @Test + public void testWrite38() throws IOException { + int[][] output = { + { Integer.MAX_VALUE, 6, Integer.MAX_VALUE - 1 }, + { Integer.MIN_VALUE, 8, Integer.MIN_VALUE + 1 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + outMdata.setDataCompression(CbfCompression.NONE); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Int as signed 32-bit integer, without specified element type, with none + * compression. + */ + @Test + public void testWrite39() throws IOException { + int[][] output = { + { Integer.MAX_VALUE, 6, Integer.MAX_VALUE - 1 }, + { Integer.MIN_VALUE, 8, Integer.MIN_VALUE + 1 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, param)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* Float as signed 32-bit integer with none compression. */ + @Test + public void testWrite40() throws IOException { + float[][] output = { + { Integer.MAX_VALUE, 6, 7 }, + { Integer.MAX_VALUE, 0, Integer.MAX_VALUE - 1 } + }; + float[][] expected = output; + + BufferedImage outImage = createFloatBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + outMdata.setDataCompression(CbfCompression.NONE); + byte[] data = writeBufferedImage(outImage, outMdata); + CbfImageReader reader = createReader(); + reader.setInput(createInputStream(data)); + ImageReadParam param = reader.getDefaultReadParam(); + boolean foundFloatType = false; + for (Iterator i = reader.getImageTypes(0); i.hasNext();) { + ImageTypeSpecifier each = i.next(); + if (each.getSampleModel().getDataType() != DataBuffer.TYPE_FLOAT) continue; + param.setDestinationType(each); + foundFloatType = true; + } + assertTrue(foundFloatType); + BufferedImage inImage = reader.read(0, param); + CbfMetadata inMdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_FLOAT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* Float as signed 32-bit real IEEE with none compression. */ + @Test + public void testWrite41() throws IOException { + float[][] output = { + { Float.MAX_VALUE, 6, Float.MAX_VALUE - 1 }, + { Float.MIN_VALUE, 8, Float.MIN_VALUE + 1 }, + { 1.234331235e20f, 8, -1.23423123925e-20f } + }; + float[][] expected = output; + + BufferedImage outImage = createFloatBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_REAL_IEEE); + outMdata.setDataCompression(CbfCompression.NONE); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_REAL_IEEE, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_FLOAT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* + * Float as signed 32-bit real IEEE, without specified element type, with + * none compression. + */ + @Test + public void testWrite42() throws IOException { + float[][] output = { + { Float.MAX_VALUE, 6, Float.MAX_VALUE - 1 }, + { Float.MIN_VALUE, 8, Float.MIN_VALUE + 1 }, + { 1.234331235e20f, 8, -1.23423123925e-20f } + }; + float[][] expected = output; + + BufferedImage outImage = createFloatBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, param)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_REAL_IEEE, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_FLOAT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* Double as signed 64-bit real IEEE with none compression. */ + @Test + public void testWrite43() throws IOException { + double[][] output = { + { Double.MAX_VALUE, 6, Double.MAX_VALUE - 1 }, + { Double.MIN_VALUE, 8, Double.MIN_VALUE + 1 }, + { 1.2343423235e201, 8, -1.234342129251e-201 } + }; + double[][] expected = output; + + BufferedImage outImage = createDoubleBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_64_BIT_REAL_IEEE); + outMdata.setDataCompression(CbfCompression.NONE); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_64_BIT_REAL_IEEE, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_DOUBLE, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getDoublePixels(inImage)); + } + + /* + * Double as signed 64-bit real IEEE, without specified element type, with + * none compression. + */ + @Test + public void testWrite44() throws IOException { + double[][] output = { + { Double.MAX_VALUE, 6, Double.MAX_VALUE - 1 }, + { Double.MIN_VALUE, 8, Double.MIN_VALUE + 1 }, + { 1.2343423235e201, 8, -1.234342129251e-201 } + }; + double[][] expected = output; + + BufferedImage outImage = createDoubleBufferedImage(output); + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, param)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_64_BIT_REAL_IEEE, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_DOUBLE, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getDoublePixels(inImage)); + } + + /* ImageWriteParam compression overrides CbfMetadata compression. */ + @Test + public void testWrite45() throws IOException { + byte[][] output = { + { -1, -2, -3 }, + { +4, +5, +6 } + }; + + BufferedImage outImage = createByteBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataCompression(CbfCompression.NONE); + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType(CbfImageWriteParam.COMPRESSION_BYTE_OFFSET); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, mdata, param)); + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + } + + /* ImageWriteParam compression overrides CbfMetadata compression. */ + @Test + public void testWrite46() throws IOException { + byte[][] output = { + { -1, -2, -3 }, + { +4, +5, +6 } + }; + + BufferedImage outImage = createByteBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataCompression(CbfCompression.BYTE_OFFSET); + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType(CbfImageWriteParam.COMPRESSION_NONE); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, mdata, param)); + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + } + + /* Progress for byte_offset compression. */ + @Test + public void testWrite47() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + final List started = Arrays.asList(new Boolean[] { Boolean.FALSE }); + final List percentDone = new ArrayList(); + final List completed = Arrays.asList(new Boolean[] { Boolean.FALSE }); + final List aborted = Arrays.asList(new Boolean[] { Boolean.FALSE }); + IIOWriteProgressListener listener = new IIOWriteProgressListener() { + public void imageStarted(ImageWriter source, int imageIndex) { + started.set(0, Boolean.TRUE); + } + + public void imageProgress(ImageWriter source, float percentageDone) { + percentDone.add(percentageDone); + } + + public void imageComplete(ImageWriter source) { + completed.set(0, Boolean.TRUE); + } + + public void thumbnailStarted(ImageWriter source, int imageIndex, int thumbnailIndex) { + } + + public void thumbnailProgress(ImageWriter source, float percentageDone) { + } + + public void thumbnailComplete(ImageWriter source) { + } + + public void writeAborted(ImageWriter source) { + aborted.set(0, Boolean.TRUE); + } + }; + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType(CbfImageWriteParam.COMPRESSION_BYTE_OFFSET); + writeBufferedImage(outImage, param, listener); + assertTrue(started.get(0)); + assertTrue(completed.get(0)); + assertFalse(aborted.get(0)); + assertEquals(5, percentDone.size()); + assertEquals(80.0f, percentDone.get(percentDone.size() - 1), 0.1f); + } + + /* Progress for none compression. */ + @Test + public void testWrite48() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, 15 }, + { 16, 17, 18, 19, 20, 21 }, + { 22, 23, 24, 25, 26, 27 }, + { 28, 29, 30, 31, 32, 33 }, + { 34, 35, 36, 37, 38, 39 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + final List started = Arrays.asList(new Boolean[] { Boolean.FALSE }); + final List percentDone = new ArrayList(); + final List completed = Arrays.asList(new Boolean[] { Boolean.FALSE }); + final List aborted = Arrays.asList(new Boolean[] { Boolean.FALSE }); + IIOWriteProgressListener listener = new IIOWriteProgressListener() { + public void imageStarted(ImageWriter source, int imageIndex) { + started.set(0, Boolean.TRUE); + } + + public void imageProgress(ImageWriter source, float percentageDone) { + percentDone.add(percentageDone); + } + + public void imageComplete(ImageWriter source) { + completed.set(0, Boolean.TRUE); + } + + public void thumbnailStarted(ImageWriter source, int imageIndex, int thumbnailIndex) { + } + + public void thumbnailProgress(ImageWriter source, float percentageDone) { + } + + public void thumbnailComplete(ImageWriter source) { + } + + public void writeAborted(ImageWriter source) { + aborted.set(0, Boolean.TRUE); + } + }; + ImageWriteParam param = createImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + writeBufferedImage(outImage, param, listener); + assertTrue(started.get(0)); + assertTrue(completed.get(0)); + assertFalse(aborted.get(0)); + assertEquals(5, percentDone.size()); + assertEquals(80.0f, percentDone.get(percentDone.size() - 1), 0.1f); + } + + /* Int as signed 32-bit integer with byte_offset compression. */ + @Test + public void testWrite49() throws IOException { + int[][] output = { + { 10, 11, 12, 13, 14, Integer.MAX_VALUE }, + { 16, 17, 18, 19, 20, Integer.MIN_VALUE }, + { 22, 23, 24, 25, 26, Short.MAX_VALUE + 51 }, + { 28, 29, 30, 31, 32, 33 }, + { 28, 29, 30, 31, 32, 34 }, + { 34, 35, 36, 37, 38, 39 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Second band (index 1) of RGB source image as signed 32-bit integer with + * byte_offset compression. + */ + @Test + public void testWrite50() throws IOException { + int[][][] output = { + { { 0, 0, 0 }, { 0, +1, 0 }, { 0, +2, 0 } }, + { { 0, 3, 0 }, { 0, +4, 0 }, { 0, +5, 0 } }, + { { 0, 6, 0 }, { 0, +7, 0 }, { 0, +8, 0 } }, + { { 0, 9, 0 }, { 0, 10, 0 }, { 0, 11, 0 } } + }; + int[][] expected = new int[output.length][output[0].length]; + for (int r = 0; r < expected.length; r++) { + for (int c = 0; c < expected[0].length; c++) { + expected[r][c] = output[r][c][1]; + } + } + + BufferedImage outImage = createIntRgbBufferedImage(output); + ImageWriteParam outParam = createImageWriteParam(); + outParam.setSourceBands(new int[] { 1 }); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, outMdata, outParam)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Second band (index 1) of RGB source image as signed 32-bit integer with + * none compression. + */ + @Test + public void testWrite51() throws IOException { + int[][][] output = { + { { 0, 0, 0 }, { 0, +1, 0 }, { 0, +2, 0 } }, + { { 0, 3, 0 }, { 0, +4, 0 }, { 0, +5, 0 } }, + { { 0, 6, 0 }, { 0, +7, 0 }, { 0, +8, 0 } }, + { { 0, 9, 0 }, { 0, 10, 0 }, { 0, 11, 0 } } + }; + int[][] expected = new int[output.length][output[0].length]; + for (int r = 0; r < expected.length; r++) { + for (int c = 0; c < expected[0].length; c++) { + expected[r][c] = output[r][c][1]; + } + } + + BufferedImage outImage = createIntRgbBufferedImage(output); + ImageWriteParam outParam = createImageWriteParam(); + outParam.setSourceBands(new int[] { 1 }); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.SIGNED_32_BIT_INTEGER); + outMdata.setDataCompression(CbfCompression.NONE); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, outMdata, outParam)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Float, including negative values, as unsigned 32-bit integer with + * byte_offset compression. Negative values are clipped to 0. + */ + @Test + public void testWrite52() throws IOException { + float[][] output = { + { Integer.MIN_VALUE - 40.0f, -6.0f, Integer.MAX_VALUE + 50.0f }, + { Integer.MIN_VALUE - 41.0f, +8.0f, Integer.MAX_VALUE + 51.0f } + }; + float[][] expected = { + { 0.0f, 0.0f, Integer.MAX_VALUE + 50.0f }, + { 0.0f, 8.0f, Integer.MAX_VALUE + 51.0f } + }; + + BufferedImage outImage = createFloatBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.UNSIGNED_32_BIT_INTEGER); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, outMdata)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_FLOAT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* + * Int, including negative values, as unsigned 32-bit integer with + * byte_offset compression. Negative values are clipped to 0. + */ + @Test + public void testWrite53() throws IOException { + int[][] output = { + { Integer.MIN_VALUE + 0, -6, -1 }, + { Integer.MIN_VALUE + 1, +0, +7 } + }; + int[][] expected = { + { 0, 0, 0 }, + { 0, 0, 7 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.UNSIGNED_32_BIT_INTEGER); + byte[] data = writeBufferedImage(outImage, outMdata); + CbfImageReader reader = createReader(); + reader.setInput(createInputStream(data)); + ImageReadParam param = reader.getDefaultReadParam(); + boolean foundIntType = false; + for (Iterator i = reader.getImageTypes(0); i.hasNext();) { + ImageTypeSpecifier each = i.next(); + if (each.getSampleModel().getDataType() != DataBuffer.TYPE_INT) continue; + param.setDestinationType(each); + foundIntType = true; + } + assertTrue(foundIntType); + BufferedImage inImage = reader.read(0, param); + CbfMetadata inMdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(CbfElementType.UNSIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Float, including negative values, as unsigned 32-bit integer with none + * compression. Negative values are clipped to 0. + */ + @Test + public void testWrite54() throws IOException { + float[][] output = { + { Integer.MIN_VALUE - 40.0f, -6.0f, Integer.MAX_VALUE + 50.0f }, + { Integer.MIN_VALUE - 41.0f, +8.0f, Integer.MAX_VALUE + 51.0f } + }; + float[][] expected = { + { 0.0f, 0.0f, Integer.MAX_VALUE + 50.0f }, + { 0.0f, 8.0f, Integer.MAX_VALUE + 51.0f } + }; + + BufferedImage outImage = createFloatBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.UNSIGNED_32_BIT_INTEGER); + outMdata.setDataCompression(CbfCompression.NONE); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, outMdata)); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_FLOAT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getFloatPixels(inImage)); + } + + /* + * Int, including negative values, as unsigned 32-bit integer with none + * compression. Negative values are clipped to 0. + */ + @Test + public void testWrite55() throws IOException { + int[][] output = { + { Integer.MIN_VALUE + 0, -6, -1 }, + { Integer.MIN_VALUE + 1, +0, +7 } + }; + int[][] expected = { + { 0, 0, 0 }, + { 0, 0, 7 } + }; + + BufferedImage outImage = createIntBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(CbfElementType.UNSIGNED_32_BIT_INTEGER); + outMdata.setDataCompression(CbfCompression.NONE); + byte[] data = writeBufferedImage(outImage, outMdata); + CbfImageReader reader = createReader(); + reader.setInput(createInputStream(data)); + ImageReadParam param = reader.getDefaultReadParam(); + boolean foundIntType = false; + for (Iterator i = reader.getImageTypes(0); i.hasNext();) { + ImageTypeSpecifier each = i.next(); + if (each.getSampleModel().getDataType() != DataBuffer.TYPE_INT) continue; + param.setDestinationType(each); + foundIntType = true; + } + assertTrue(foundIntType); + BufferedImage inImage = reader.read(0, param); + CbfMetadata inMdata = (CbfMetadata)reader.getImageMetadata(0); + assertEquals(CbfElementType.UNSIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.NONE, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Short, including negative values, as unsigned 16-bit integer with + * byte_offset compression. Negative values are clipped to 0. + */ + @Test + public void testWrite56() throws IOException { + short[][] output = { + { Short.MIN_VALUE + 0, 0, +1, +2, +3 }, + { Short.MIN_VALUE + 1, 0, -1, -2, -3 } + }; + int[][] expected = { + { 0, 0, +1, +2, +3 }, + { 0, 0, +0, +0, +0 } + }; + + BufferedImage outImage = createShortBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_16_BIT_INTEGER); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_USHORT_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_USHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Short, including negative values, as unsigned 16-bit integer with none + * compression. Negative values are clipped to 0. + */ + @Test + public void testWrite57() throws IOException { + short[][] output = { + { Short.MIN_VALUE + 0, 0, +1, +2, +3 }, + { Short.MIN_VALUE + 1, 0, -1, -2, -3 } + }; + int[][] expected = { + { 0, 0, +1, +2, +3 }, + { 0, 0, +0, +0, +0 } + }; + + BufferedImage outImage = createShortBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_16_BIT_INTEGER); + mdata.setDataCompression(CbfCompression.NONE); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_USHORT_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_USHORT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + /* + * Byte, including negative values, as unsigned 8-bit integer with + * byte_offset compression. Negative values are clipped to 0. + */ + @Test + public void testWrite58() throws IOException { + byte[][] output = { + { Byte.MIN_VALUE + 0, 0, +1, +2, +3 }, + { Byte.MIN_VALUE + 1, 0, -1, -2, -3 } + }; + short[][] expected = { + { 0, 0, +1, +2, +3 }, + { 0, 0, +0, +0, +0 } + }; + + BufferedImage outImage = createByteBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_8_BIT_INTEGER); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_BYTE_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_BYTE, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getShortPixels(inImage)); + } + + /* + * Byte, including negative values, as unsigned 16-bit integer with none + * compression. Negative values are clipped to 0. + */ + @Test + public void testWrite59() throws IOException { + byte[][] output = { + { Byte.MIN_VALUE + 0, 0, +1, +2, +3 }, + { Byte.MIN_VALUE + 1, 0, -1, -2, -3 } + }; + short[][] expected = { + { 0, 0, +1, +2, +3 }, + { 0, 0, +0, +0, +0 } + }; + + BufferedImage outImage = createByteBufferedImage(output); + CbfMetadata mdata = new CbfMetadata(); + mdata.setDataElementType(CbfElementType.UNSIGNED_8_BIT_INTEGER); + mdata.setDataCompression(CbfCompression.NONE); + BufferedImage inImage = readBufferedImage(writeBufferedImage(outImage, mdata)); + assertEquals(BufferedImage.TYPE_BYTE_GRAY, inImage.getType()); + assertEquals(DataBuffer.TYPE_BYTE, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getShortPixels(inImage)); + } + + /* Null data element type should default to signed 32-bit integer. */ + @Test + public void testWrite60() throws IOException { + int[][] output = { + { Integer.MAX_VALUE, 6, Integer.MAX_VALUE - 1 }, + { Integer.MIN_VALUE, 8, Integer.MIN_VALUE + 1 } + }; + int[][] expected = output; + + BufferedImage outImage = createIntBufferedImage(output); + CbfMetadata outMdata = new CbfMetadata(); + outMdata.setDataElementType(null); + Object[] inImageAndMetadata = + readBufferedImageAndMetadata(writeBufferedImage(outImage, outMdata)); + BufferedImage inImage = (BufferedImage)inImageAndMetadata[0]; + CbfMetadata inMdata = (CbfMetadata)inImageAndMetadata[1]; + assertEquals(CbfElementType.SIGNED_32_BIT_INTEGER, inMdata.getDataElementType()); + assertEquals(CbfCompression.BYTE_OFFSET, inMdata.getDataCompression()); + assertEquals(BufferedImage.TYPE_CUSTOM, inImage.getType()); + assertEquals(DataBuffer.TYPE_INT, inImage.getSampleModel().getDataType()); + assertArrayEquals(expected, getIntPixels(inImage)); + } + + private ImageOutputStream createOutputStream(OutputStream stream) { + return new MemoryCacheImageOutputStream(stream); + } + + private CbfImageWriter createWriter(ImageOutputStream stream) throws IOException { + CbfImageWriterSpi spi = new CbfImageWriterSpi(); + CbfImageWriter writer = (CbfImageWriter)spi.createWriterInstance(); + writer.setOutput(stream); + return writer; + } + + private IIOImage create1x1Image(IIOMetadata metadata) { + return new IIOImage(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY), null, metadata); + } + + private String extractHeader(byte[] image) { + try { + InputStream stream = new ByteArrayInputStream(image); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "US-ASCII")); + StringBuffer result = new StringBuffer(); + String line = reader.readLine(); + while (line != null && !line.equals("_array_data.header_contents")) { + line = reader.readLine(); + } + line = reader.readLine(); + if (line == null || !line.equals(";")) fail("Did not find ';' line at start of header"); + line = reader.readLine(); + while (line != null && !line.equals(";")) { + result.append(line + "\r\n"); + line = reader.readLine(); + } + return result.toString(); + } catch (Exception e) { + fail(e.toString()); + return null; + } + } + + private CbfMetadata createMetadata() { + CbfMetadata result = new CbfMetadata(); + result.setHeaderDefaults(); + Calendar c = Calendar.getInstance(Locale.US); + c.set(Calendar.YEAR, 2010); + c.set(Calendar.MONTH, 10); + c.set(Calendar.DAY_OF_MONTH, 17); + c.set(Calendar.HOUR_OF_DAY, 15); + c.set(Calendar.MINUTE, 52); + c.set(Calendar.SECOND, 30); + c.set(Calendar.MILLISECOND, 834); + result.setHeaderDate(c.getTime()); + return result; + } +} diff --git a/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfMetadataTest.java b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfMetadataTest.java new file mode 100644 index 0000000..812a62a --- /dev/null +++ b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/CbfMetadataTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import static junit.framework.Assert.assertNull; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * I test CbfMetadata. + */ +public class CbfMetadataTest extends TestUtils { + @Test(expected = IllegalArgumentException.class) + public void testSetHeaderDetector01() { + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderDetector("PILATUS 6M\r\nHosed"); + } + + @Test + public void testSetHeaderDetector02() { + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderDetector(null); + assertNull(mdata.getHeaderDetectorName()); + assertNull(mdata.getHeaderDetectorSerialNumber()); + assertNull(mdata.getHeaderDetectorDescription()); + } + + @Test + public void testSetHeaderDetector03() { + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderDetector("PILATUS 6M, 60-0002, IMCA-CAT"); + assertEquals("PILATUS 6M", mdata.getHeaderDetectorName()); + assertEquals("60-0002", mdata.getHeaderDetectorSerialNumber()); + assertEquals("IMCA-CAT", mdata.getHeaderDetectorDescription()); + } + + @Test + public void testSetHeaderDetector04() { + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderDetector("PILATUS 6M, 60-0001, X06SA@SLS"); + assertEquals("PILATUS 6M", mdata.getHeaderDetectorName()); + assertEquals("60-0001", mdata.getHeaderDetectorSerialNumber()); + assertEquals("X06SA@SLS", mdata.getHeaderDetectorDescription()); + } + + @Test + public void testSetHeaderDetector05() { + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderDetector("PILATUS 6M SN: 60-0003"); + assertEquals("PILATUS 6M", mdata.getHeaderDetectorName()); + assertEquals("60-0003", mdata.getHeaderDetectorSerialNumber()); + assertNull(mdata.getHeaderDetectorDescription()); + } + + @Test + public void testSetHeaderDetector06() { + CbfMetadata mdata = new CbfMetadata(); + mdata.setHeaderDetector("PILATUS 6M IMCA-CAT"); + assertNull(mdata.getHeaderDetectorName()); + assertNull(mdata.getHeaderDetectorSerialNumber()); + assertNull(mdata.getHeaderDetectorDescription()); + } + + @Test + public void testGetAsTree01() { + (new CbfMetadata()).getAsTree(CbfMetadata.NATIVE_FORMAT_NAME); + } +} diff --git a/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/TestUtils.java b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/TestUtils.java new file mode 100644 index 0000000..2e9ece2 --- /dev/null +++ b/ch.psi.imagej.cbf/src/test/java/imcacat/jcbf/TestUtils.java @@ -0,0 +1,474 @@ +/* + * Copyright (c) 2011 J. Lewis Muir + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package imcacat.jcbf; + +import static org.junit.Assert.fail; + +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import javax.imageio.IIOImage; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.event.IIOReadProgressListener; +import javax.imageio.event.IIOWriteProgressListener; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; +import javax.imageio.stream.MemoryCacheImageOutputStream; + +/** + * I provide utility methods for use in testing. + */ +public class TestUtils { + public static ImageInputStream streamFromString(String s) { + return streamFromBytes(asciiBytes(s)); + } + + public static ImageInputStream streamFromBytes(byte[] b) { + return new MemoryCacheImageInputStream(new ByteArrayInputStream(b)); + } + + public static byte[] asciiBytes(String value) { + try { + return value.getBytes("US-ASCII"); + } catch (UnsupportedEncodingException e) { + fail(e.toString()); + return new byte[] {}; + } + } + + protected static String createImageText() { + return "###CBF: VERSION 1.5, CBFlib v0.7.8 - SLS/DECTRIS PILATUS detectors\r\n" + + "\r\n" + + "data_thaumatin_0001\r\n" + + "\r\n" + + "_array_data.header_convention \"SLS_1.0\"\r\n" + + "_array_data.header_contents\r\n" + + ";\r\n" + + "# Detector: PILATUS 6M, 60-0103, IMCA-CAT\r\n" + + "# 2010-Nov-17T15:52:30.834\r\n" + + "# Pixel_size 172e-6 m x 172e-6 m\r\n" + + "# Silicon sensor, thickness 0.000320 m\r\n" + + "# Exposure_time 0.081033 s\r\n" + + "# Exposure_period 0.083333 s\r\n" + + "# Tau = 124.0e-09 s\r\n" + + "# Count_cutoff 326741 counts\r\n" + + "# Threshold_setting 7439 eV\r\n" + + "# Gain_setting not implemented (vrf = 9.900)\r\n" + + "# N_excluded_pixels = 2391\r\n" + + "# Excluded_pixels: badpix_mask.tif\r\n" + + "# Flat_field: (nil)\r\n" + + "# Trim_directory: \r\n" + + "# Wavelength 1.0000 A\r\n" + + "# Energy_range (0, 0) eV\r\n" + + "# Detector_distance 0.94997 m\r\n" + + "# Detector_Voffset 0.00000 m\r\n" + + "# Beam_xy (1253.00, 1288.00) pixels\r\n" + + "# Flux 0.0000 ph/s\r\n" + + "# Filter_transmission 0.0000\r\n" + + "# Start_angle 0.0000 deg.\r\n" + + "# Angle_increment 1.0000 deg.\r\n" + + "# Detector_2theta 0.0007 deg.\r\n" + + "# Polarization 0.990\r\n" + + "# Alpha 0.0000 deg.\r\n" + + "# Kappa 0.0000 deg.\r\n" + + "# Phi 0.0000 deg.\r\n" + + "# Chi 0.0000 deg.\r\n" + + "# Oscillation_axis X, CW\r\n" + + "# N_oscillations 1\r\n" + + ";\r\n" + + "\r\n" + + "_array_data.data\r\n" + + ";\r\n" + + "--CIF-BINARY-FORMAT-SECTION--\r\n" + + "Content-Type: application/octet-stream;\r\n" + + " conversions=\"x-CBF_BYTE_OFFSET\"\r\n" + + "Content-Transfer-Encoding: BINARY\r\n" + + "X-Binary-Size: 6224001\r\n" + + "X-Binary-ID: 1\r\n" + + "X-Binary-Element-Type: \"signed 32-bit integer\"\r\n" + + "X-Binary-Element-Byte-Order: LITTLE_ENDIAN\r\n" + + "Content-MD5: Ti67wd6I6DPuGIsOnMalMw==\r\n" + + "X-Binary-Number-of-Elements: 6224001\r\n" + + "X-Binary-Size-Fastest-Dimension: 2463\r\n" + + "X-Binary-Size-Second-Dimension: 2527\r\n" + + "X-Binary-Size-Padding: 4095\r\n" + + "\r\n"; + } + + protected static byte[] createImageWithoutData() { + return createImageWithoutData(asciiBytes(createImageText())); + } + + protected static byte[] createImageWithoutData(byte[] text) { + byte[] result = new byte[text.length + 4]; + System.arraycopy(text, 0, result, 0, text.length); + result[result.length - 4] = 0x0C; + result[result.length - 3] = 0x1A; + result[result.length - 2] = 0x04; + result[result.length - 1] = (byte)0xD5; + return result; + } + + protected static ImageInputStream createInputStream(byte[] cbf) { + return new MemoryCacheImageInputStream(new ByteArrayInputStream(cbf)); + } + + protected static BufferedImage createByteBinaryBufferedImage(byte[][] pixels) { + BufferedImage image = createByteBinaryBufferedImage(pixels[0].length, pixels.length); + WritableRaster raster = image.getRaster(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + raster.setSample(x, y, 0, pixels[y][x]); + } + } + return image; + } + + protected static BufferedImage createByteBinaryBufferedImage(int width, int height) { + return createByteBinarySpecifier().createBufferedImage(width, height); + } + + protected static ImageTypeSpecifier createByteBinarySpecifier() { + return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_BINARY); + } + + protected static BufferedImage createUbyteBufferedImage(short[][] pixels) { + BufferedImage image = createUbyteBufferedImage(pixels[0].length, pixels.length); + WritableRaster raster = image.getRaster(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + raster.setSample(x, y, 0, pixels[y][x]); + } + } + return image; + } + + protected static BufferedImage createUbyteBufferedImage(int width, int height) { + return createUbyteSpecifier().createBufferedImage(width, height); + } + + protected static ImageTypeSpecifier createUbyteSpecifier() { + return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY); + } + + protected static BufferedImage createByteBufferedImage(byte[][] pixels) { + BufferedImage image = createByteBufferedImage(pixels[0].length, pixels.length); + WritableRaster raster = image.getRaster(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + raster.setSample(x, y, 0, pixels[y][x]); + } + } + return image; + } + + protected static BufferedImage createByteBufferedImage(int width, int height) { + return createByteSpecifier().createBufferedImage(width, height); + } + + protected static ImageTypeSpecifier createByteSpecifier() { + /* + * The type DataBuffer.TYPE_BYTE is for unsigned byte data so must use + * DataBuffer.TYPE_SHORT. ImageTypeSpecifier.createGrayscale does not + * support 8 bits with DataBuffer.TYPE_SHORT, so must use 16. + */ + return ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, true); + } + + protected static BufferedImage createUshortBufferedImage(int[][] pixels) { + BufferedImage image = createUshortBufferedImage(pixels[0].length, pixels.length); + WritableRaster raster = image.getRaster(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + raster.setSample(x, y, 0, pixels[y][x]); + } + } + return image; + } + + protected static BufferedImage createUshortBufferedImage(int width, int height) { + return createUshortSpecifier().createBufferedImage(width, height); + } + + protected static ImageTypeSpecifier createUshortSpecifier() { + return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY); + } + + protected static BufferedImage createShortBufferedImage(short[][] pixels) { + BufferedImage image = createShortBufferedImage(pixels[0].length, pixels.length); + WritableRaster raster = image.getRaster(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + raster.setSample(x, y, 0, pixels[y][x]); + } + } + return image; + } + + protected static BufferedImage createShortBufferedImage(int width, int height) { + return createShortSpecifier().createBufferedImage(width, height); + } + + protected static ImageTypeSpecifier createShortSpecifier() { + return ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, true); + } + + protected static BufferedImage createIntBufferedImage(int[][] pixels) { + BufferedImage image = createIntBufferedImage(pixels[0].length, pixels.length); + WritableRaster raster = image.getRaster(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + raster.setSample(x, y, 0, pixels[y][x]); + } + } + return image; + } + + protected static BufferedImage createIntBufferedImage(int width, int height) { + return createIntSpecifier().createBufferedImage(width, height); + } + + protected static ImageTypeSpecifier createIntSpecifier() { + return ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_GRAY), + new int[] { 0 }, DataBuffer.TYPE_INT, false, false); + } + + protected static BufferedImage createIntRgbBufferedImage(int[][][] rgbPixels) { + BufferedImage image = createIntRgbBufferedImage(rgbPixels[0].length, rgbPixels.length); + WritableRaster raster = image.getRaster(); + for (int y = 0; y < rgbPixels.length; y++) { + for (int x = 0; x < rgbPixels[0].length; x++) { + raster.setPixel(x, y, rgbPixels[y][x]); + } + } + return image; + } + + protected static BufferedImage createIntRgbBufferedImage(int width, int height) { + return createIntRgbSpecifier().createBufferedImage(width, height); + } + + protected static ImageTypeSpecifier createIntRgbSpecifier() { + return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB); + } + + protected static BufferedImage createFloatBufferedImage(float[][] pixels) { + BufferedImage image = createFloatBufferedImage(pixels[0].length, pixels.length); + WritableRaster raster = image.getRaster(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + raster.setSample(x, y, 0, pixels[y][x]); + } + } + return image; + } + + protected static BufferedImage createFloatBufferedImage(int width, int height) { + return createFloatSpecifier().createBufferedImage(width, height); + } + + protected static ImageTypeSpecifier createFloatSpecifier() { + return ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_GRAY), + new int[] { 0 }, DataBuffer.TYPE_FLOAT, false, false); + } + + protected static BufferedImage createDoubleBufferedImage(double[][] pixels) { + BufferedImage image = createDoubleBufferedImage(pixels[0].length, pixels.length); + WritableRaster raster = image.getRaster(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + raster.setSample(x, y, 0, pixels[y][x]); + } + } + return image; + } + + protected static BufferedImage createDoubleBufferedImage(int width, int height) { + return createDoubleSpecifier().createBufferedImage(width, height); + } + + protected static ImageTypeSpecifier createDoubleSpecifier() { + return ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_GRAY), + new int[] { 0 }, DataBuffer.TYPE_DOUBLE, false, false); + } + + protected static byte[][] getBytePixels(BufferedImage image) { + byte[][] pixels = new byte[image.getHeight()][image.getWidth()]; + Raster raster = image.getData(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + pixels[y][x] = (byte)raster.getSample(x, y, 0); + } + } + return pixels; + } + + protected static short[][] getShortPixels(BufferedImage image) { + short[][] pixels = new short[image.getHeight()][image.getWidth()]; + Raster raster = image.getData(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + pixels[y][x] = (short)raster.getSample(x, y, 0); + } + } + return pixels; + } + + protected static int[][] getIntPixels(BufferedImage image) { + int[][] pixels = new int[image.getHeight()][image.getWidth()]; + Raster raster = image.getData(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + pixels[y][x] = raster.getSample(x, y, 0); + } + } + return pixels; + } + + protected static int[][][] getIntRgbPixels(BufferedImage image) { + int[][][] pixels = new int[image.getHeight()][image.getWidth()][3]; + Raster raster = image.getData(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + raster.getPixel(x, y, pixels[y][x]); + } + } + return pixels; + } + + protected static float[][] getFloatPixels(BufferedImage image) { + float[][] pixels = new float[image.getHeight()][image.getWidth()]; + Raster raster = image.getData(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + pixels[y][x] = raster.getSampleFloat(x, y, 0); + } + } + return pixels; + } + + protected static double[][] getDoublePixels(BufferedImage image) { + double[][] pixels = new double[image.getHeight()][image.getWidth()]; + Raster raster = image.getData(); + for (int y = 0; y < pixels.length; y++) { + for (int x = 0; x < pixels[0].length; x++) { + pixels[y][x] = raster.getSampleDouble(x, y, 0); + } + } + return pixels; + } + + protected static BufferedImage readBufferedImage(byte[] input) throws IOException { + return readBufferedImage(input, null, null); + } + + protected static BufferedImage readBufferedImage(byte[] input, ImageReadParam param) + throws IOException { + return readBufferedImage(input, param, null); + } + + protected static BufferedImage readBufferedImage(byte[] input, IIOReadProgressListener listener) + throws IOException { + return readBufferedImage(input, null, listener); + } + + protected static BufferedImage readBufferedImage(byte[] input, ImageReadParam param, + IIOReadProgressListener listener) + throws IOException { + return (BufferedImage)readBufferedImageAndMetadata(input, param, listener)[0]; + } + + protected static Object[] readBufferedImageAndMetadata(byte[] input) throws IOException { + return readBufferedImageAndMetadata(input, null, null); + } + + protected static Object[] readBufferedImageAndMetadata(byte[] input, ImageReadParam param, + IIOReadProgressListener listener) throws IOException { + ByteArrayInputStream inBytesStream = new ByteArrayInputStream(input); + ImageInputStream inStream = new MemoryCacheImageInputStream(inBytesStream); + CbfImageReader reader = createReader(); + if (listener != null) reader.addIIOReadProgressListener(listener); + reader.setInput(inStream); + BufferedImage result = reader.read(0, param); + CbfMetadata mdata = (CbfMetadata)reader.getImageMetadata(0); + if (listener != null) reader.removeIIOReadProgressListener(listener); + return new Object[] { result, mdata }; + } + + protected static CbfImageReader createReader() throws IOException { + return (CbfImageReader)((new CbfImageReaderSpi()).createReaderInstance()); + } + + protected static byte[] writeBufferedImage(BufferedImage image) throws IOException { + return writeBufferedImage(image, null, null, null); + } + + protected static byte[] writeBufferedImage(BufferedImage image, CbfMetadata mdata) + throws IOException { + return writeBufferedImage(image, mdata, null); + } + + protected static byte[] writeBufferedImage(BufferedImage image, ImageWriteParam param) + throws IOException { + return writeBufferedImage(image, null, param); + } + + protected static byte[] writeBufferedImage(BufferedImage image, ImageWriteParam param, + IIOWriteProgressListener listener) throws IOException { + return writeBufferedImage(image, null, param, listener); + } + + protected static byte[] writeBufferedImage(BufferedImage image, CbfMetadata mdata, + ImageWriteParam param) throws IOException { + return writeBufferedImage(image, mdata, param, null); + } + + protected static byte[] writeBufferedImage(BufferedImage image, CbfMetadata mdata, + ImageWriteParam param, IIOWriteProgressListener listener) throws IOException { + ByteArrayOutputStream outBytesStream = new ByteArrayOutputStream(); + ImageOutputStream outStream = new MemoryCacheImageOutputStream(outBytesStream); + CbfImageWriterSpi writerSpi = new CbfImageWriterSpi(); + CbfImageWriter writer = (CbfImageWriter)writerSpi.createWriterInstance(); + if (listener != null) writer.addIIOWriteProgressListener(listener); + writer.setOutput(outStream); + writer.write(null, new IIOImage(image, null, mdata), param); + if (listener != null) writer.removeIIOWriteProgressListener(listener); + return outBytesStream.toByteArray(); + } + + protected static ImageReadParam createImageReadParam() throws IOException { + CbfImageReaderSpi spi = new CbfImageReaderSpi(); + CbfImageReader reader = (CbfImageReader)spi.createReaderInstance(); + return reader.getDefaultReadParam(); + } + + protected static ImageWriteParam createImageWriteParam() throws IOException { + CbfImageWriterSpi spi = new CbfImageWriterSpi(); + CbfImageWriter writer = (CbfImageWriter)spi.createWriterInstance(); + return writer.getDefaultWriteParam(); + } +} diff --git a/ch.psi.imagej.cbf/src/test/resources/.gitignore b/ch.psi.imagej.cbf/src/test/resources/.gitignore new file mode 100644 index 0000000..e69de29