//
// SlidebookReader.java
//
/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package loci.formats.in;
import java.io.EOFException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Vector;
import loci.common.RandomAccessInputStream;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
import ome.xml.model.primitives.PositiveFloat;
/**
* SlidebookReader is the file format reader for 3I Slidebook files.
* The strategies employed by this reader are highly suboptimal, as we
* have very little information on the Slidebook format.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/in/SlidebookReader.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/SlidebookReader.java;hb=HEAD">Gitweb</a></dd></dl>
*
* @author Melissa Linkert melissa at glencoesoftware.com
*/
public class SlidebookReader extends FormatReader {
// -- Constants --
public static final int SLD_MAGIC_BYTES_1_0 = 0x006c;
public static final int SLD_MAGIC_BYTES_1_1 = 0x0100;
public static final int SLD_MAGIC_BYTES_2_0 = 0x01f5;
public static final int SLD_MAGIC_BYTES_2_1 = 0x0102;
public static final long SLD_MAGIC_BYTES_3 = 0xf6010101L;
// -- Fields --
private Vector<Long> metadataOffsets;
private Vector<Long> pixelOffsets;
private Vector<Long> pixelLengths;
private Vector<Double> ndFilters;
private long[][] planeOffset;
private boolean adjust = true;
private boolean isSpool;
private Hashtable<Integer, Integer> metadataInPlanes;
// -- Constructor --
/** Constructs a new Slidebook reader. */
public SlidebookReader() {
super("Olympus Slidebook", new String[] {"sld", "spl"});
domains = new String[] {FormatTools.LM_DOMAIN};
suffixSufficient = false;
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
final int blockLen = 8;
stream.seek(4);
boolean littleEndian = stream.readString(2).equals("II");
if (!FormatTools.validStream(stream, blockLen, littleEndian)) return false;
int magicBytes1 = stream.readShort();
int magicBytes2 = stream.readShort();
return ((magicBytes2 & 0xff00) == SLD_MAGIC_BYTES_1_1) &&
(magicBytes1 == SLD_MAGIC_BYTES_1_0 ||
magicBytes1 == SLD_MAGIC_BYTES_2_0);
}
/**
* @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int)
*/
public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
throws FormatException, IOException
{
FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);
long offset = planeOffset[getSeries()][no];
in.seek(offset);
// if this is a spool file, there may be an extra metadata block here
if (isSpool) {
Integer[] keys = metadataInPlanes.keySet().toArray(new Integer[0]);
Arrays.sort(keys);
for (int key : keys) {
if (key < no) {
in.skipBytes(256);
}
}
in.order(false);
long magicBytes = (long) in.readInt() & 0xffffffffL;
in.order(isLittleEndian());
if (magicBytes == SLD_MAGIC_BYTES_3 && !metadataInPlanes.contains(no)) {
metadataInPlanes.put(no, 0);
in.skipBytes(252);
}
else in.seek(in.getFilePointer() - 4);
}
readPlane(in, x, y, w, h, buf);
return buf;
}
/* @see loci.formats.IFormatReader#close(boolean) */
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
metadataOffsets = pixelOffsets = pixelLengths = null;
ndFilters = null;
isSpool = false;
metadataInPlanes = null;
adjust = true;
planeOffset = null;
}
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
in = new RandomAccessInputStream(id);
isSpool = checkSuffix(id, "spl");
if (isSpool) {
metadataInPlanes = new Hashtable<Integer, Integer>();
}
LOGGER.info("Finding offsets to pixel data");
// Slidebook files appear to be comprised of four types of blocks:
// variable length pixel data blocks, 512 byte metadata blocks,
// 128 byte metadata blocks, and variable length metadata blocks.
//
// Fixed-length metadata blocks begin with a 2 byte identifier,
// e.g. 'i' or 'h'.
// Following this are two unknown bytes (usually 256), then a 2 byte
// endianness identifier - II or MM, for little or big endian, respectively.
// Presumably these blocks contain useful information, but for the most
// part we aren't sure what it is or how to extract it.
//
// Variable length metadata blocks begin with 0xffff and are
// (as far as I know) always between two fixed-length metadata blocks.
// These appear to be a relatively new addition to the format - they are
// only present in files received on/after March 30, 2008.
//
// Each pixel data block corresponds to one series.
// The first 'i' metadata block after each pixel data block contains
// the width and height of the planes in that block - this can (and does)
// vary between blocks.
//
// Z, C, and T sizes are computed heuristically based on the number of
// metadata blocks of a specific type.
in.skipBytes(4);
core[0].littleEndian = in.read() == 0x49;
in.order(isLittleEndian());
metadataOffsets = new Vector<Long>();
pixelOffsets = new Vector<Long>();
pixelLengths = new Vector<Long>();
ndFilters = new Vector<Double>();
in.seek(0);
// gather offsets to metadata and pixel data blocks
while (in.getFilePointer() < in.length() - 8) {
LOGGER.debug("Looking for block at {}", in.getFilePointer());
in.skipBytes(4);
int checkOne = in.read();
int checkTwo = in.read();
if ((checkOne == 'I' && checkTwo == 'I') ||
(checkOne == 'M' && checkTwo == 'M'))
{
LOGGER.debug("Found metadata offset: {}", (in.getFilePointer() - 6));
metadataOffsets.add(new Long(in.getFilePointer() - 6));
in.skipBytes(in.readShort() - 8);
}
else if (checkOne == -1 && checkTwo == -1) {
boolean foundBlock = false;
byte[] block = new byte[8192];
in.read(block);
while (!foundBlock) {
for (int i=0; i<block.length-2; i++) {
if ((block[i] == 'M' && block[i + 1] == 'M') ||
(block[i] == 'I' && block[i + 1] == 'I'))
{
foundBlock = true;
in.seek(in.getFilePointer() - block.length + i - 2);
LOGGER.debug("Found metadata offset: {}",
(in.getFilePointer() - 2));
metadataOffsets.add(new Long(in.getFilePointer() - 2));
in.skipBytes(in.readShort() - 5);
break;
}
}
if (!foundBlock) {
block[0] = block[block.length - 2];
block[1] = block[block.length - 1];
in.read(block, 2, block.length - 2);
}
}
}
else {
String s = null;
long fp = in.getFilePointer() - 6;
in.seek(fp);
int len = in.read();
if (len > 0 && len <= 32) {
s = in.readString(len);
}
if (s != null && s.indexOf("Annotation") != -1) {
if (s.equals("CTimelapseAnnotation")) {
in.skipBytes(41);
if (in.read() == 0) in.skipBytes(10);
else in.seek(in.getFilePointer() - 1);
}
else if (s.equals("CIntensityBarAnnotation")) {
in.skipBytes(56);
int n = in.read();
while (n == 0 || n < 6 || n > 0x80) n = in.read();
in.seek(in.getFilePointer() - 1);
}
else if (s.equals("CCubeAnnotation")) {
in.skipBytes(66);
int n = in.read();
if (n != 0) in.seek(in.getFilePointer() - 1);
}
else if (s.equals("CScaleBarAnnotation")) {
in.skipBytes(38);
int extra = in.read();
if (extra <= 16) in.skipBytes(3 + extra);
else in.skipBytes(2);
}
}
else if (s != null && s.indexOf("Decon") != -1) {
in.seek(fp);
while (in.read() != ']');
}
else {
if ((fp % 2) == 1) fp -= 2;
in.seek(fp);
// make sure there isn't another block nearby
String checkString = in.readString(64);
if (checkString.indexOf("II") != -1 ||
checkString.indexOf("MM") != -1)
{
int index = checkString.indexOf("II");
if (index == -1) index = checkString.indexOf("MM");
in.seek(fp + index - 4);
continue;
}
else in.seek(fp);
LOGGER.debug("Found pixel offset at {}", fp);
pixelOffsets.add(new Long(fp));
try {
byte[] buf = new byte[8192];
boolean found = false;
int n = in.read(buf);
while (!found && in.getFilePointer() < in.length()) {
for (int i=0; i<n-6; i++) {
if ((buf[i + 4] == 'I' && buf[i + 5] == 'I') ||
(buf[i + 4] == 'M' && buf[i + 5] == 'M'))
{
if (((buf[i] == 'h' || buf[i] == 'i') && buf[i + 1] == 0) ||
(buf[i] == 0 && (buf[i + 1] == 'h' || buf[i + 1] == 'i')))
{
found = true;
in.seek(in.getFilePointer() - n + i - 20);
if (buf[i] == 'i' || buf[i + 1] == 'i') {
pixelOffsets.remove(pixelOffsets.size() - 1);
}
break;
}
else if (((buf[i] == 'j' || buf[i] == 'k' || buf[i] == 'n') &&
buf[i + 1] == 0) || (buf[i] == 0 && (buf[i + 1] == 'j' ||
buf[i + 1] == 'k' || buf[i + 1] == 'n')) ||
(buf[i] == 'o' && buf[i + 1] == 'n'))
{
found = true;
pixelOffsets.remove(pixelOffsets.size() - 1);
in.seek(in.getFilePointer() - n + i - 20);
break;
}
}
}
if (!found) {
byte[] tmp = buf;
buf = new byte[8192];
System.arraycopy(tmp, tmp.length - 20, buf, 0, 20);
n = in.read(buf, 20, buf.length - 20);
}
}
if (in.getFilePointer() <= in.length()) {
if (pixelOffsets.size() > pixelLengths.size()) {
long length = in.getFilePointer() - fp;
if (((length / 2) % 2) == 1) {
pixelOffsets.setElementAt(fp + 2, pixelOffsets.size() - 1);
length -= 2;
}
if (length >= 1024) {
pixelLengths.add(new Long(length));
}
else pixelOffsets.remove(pixelOffsets.size() - 1);
}
}
else pixelOffsets.remove(pixelOffsets.size() - 1);
}
catch (EOFException e) {
pixelOffsets.remove(pixelOffsets.size() - 1);
}
}
}
}
Vector<Long> orderedSeries = new Vector<Long>();
Hashtable<Long, Vector<Integer>> uniqueSeries =
new Hashtable<Long, Vector<Integer>>();
for (int i=0; i<pixelOffsets.size(); i++) {
long length = pixelLengths.get(i).longValue();
long offset = pixelOffsets.get(i).longValue();
int padding = isSpool ? 0 : 7;
if (length + offset + padding > in.length()) {
pixelOffsets.remove(i);
pixelLengths.remove(i);
i--;
}
else {
Vector<Integer> v = uniqueSeries.get(length);
if (v == null) {
orderedSeries.add(length);
v = new Vector<Integer>();
}
v.add(i);
uniqueSeries.put(length, v);
}
}
if (pixelOffsets.size() > 1) {
boolean little = isLittleEndian();
core = new CoreMetadata[uniqueSeries.size()];
for (int i=0; i<getSeriesCount(); i++) {
core[i] = new CoreMetadata();
core[i].littleEndian = little;
}
}
LOGGER.info("Determining dimensions");
// determine total number of pixel bytes
Vector<Float> pixelSize = new Vector<Float>();
String objective = null;
Vector<Double> pixelSizeZ = new Vector<Double>();
long pixelBytes = 0;
for (int i=0; i<pixelLengths.size(); i++) {
pixelBytes += pixelLengths.get(i).longValue();
}
String[] imageNames = new String[getSeriesCount()];
Vector<String> channelNames = new Vector<String>();
int nextName = 0;
int[] sizeX = new int[pixelOffsets.size()];
int[] sizeY = new int[pixelOffsets.size()];
int[] sizeZ = new int[pixelOffsets.size()];
int[] sizeC = new int[pixelOffsets.size()];
// try to find the width and height
int iCount = 0;
int hCount = 0;
int uCount = 0;
int prevSeries = -1;
int prevSeriesU = -1;
int nextChannel = 0;
for (int i=0; i<metadataOffsets.size(); i++) {
long off = metadataOffsets.get(i).longValue();
in.seek(off);
long next = i == metadataOffsets.size() - 1 ? in.length() :
metadataOffsets.get(i + 1).longValue();
int totalBlocks = (int) ((next - off) / 128);
// if there are more than 100 blocks, we probably found a pixel block
// by accident (but we'll check the first block anyway)
//if (totalBlocks > 100) totalBlocks = 100;
for (int q=0; q<totalBlocks; q++) {
if (withinPixels(off + q * 128)) {
continue;
}
in.seek(off + (long) q * 128);
char n = (char) in.readShort();
while (n == 0 && in.getFilePointer() < off + (q + 1) * 128) {
n = (char) in.readShort();
}
if (in.getFilePointer() >= in.length() - 2) break;
if (n == 'i') {
iCount++;
in.skipBytes(94);
pixelSizeZ.add(new Double(in.readFloat()));
in.seek(in.getFilePointer() - 20);
for (int j=0; j<pixelOffsets.size(); j++) {
long end = j == pixelOffsets.size() - 1 ? in.length() :
pixelOffsets.get(j + 1).longValue();
if (in.getFilePointer() < end) {
if (sizeX[j] == 0) {
int x = in.readShort();
int y = in.readShort();
if (x != 0 && y != 0) {
sizeX[j] = x;
sizeY[j] = y;
int checkX = in.readShort();
int checkY = in.readShort();
int div = in.readShort();
sizeX[j] /= (div == 0 ? 1 : div);
div = in.readShort();
sizeY[j] /= (div == 0 ? 1 : div);
}
else in.skipBytes(8);
}
if (prevSeries != j) {
iCount = 1;
}
prevSeries = j;
sizeC[j] = iCount;
break;
}
}
}
else if (n == 'u') {
uCount++;
for (int j=0; j<getSeriesCount(); j++) {
long end = j == getSeriesCount() - 1 ? in.length() :
pixelOffsets.get(j + 1).longValue();
if (in.getFilePointer() < end) {
if (prevSeriesU != j) {
uCount = 1;
}
prevSeriesU = j;
sizeZ[j] = uCount;
break;
}
}
}
else if (n == 'h') hCount++;
else if (n == 'j') {
in.skipBytes(2);
String check = in.readString(2);
if (check.equals("II") || check.equals("MM")) {
long pointer = in.getFilePointer();
// this block should contain an image name
in.skipBytes(10);
if (nextName < imageNames.length) {
imageNames[nextName++] = in.readCString().trim();
}
long fp = in.getFilePointer();
if ((in.getFilePointer() % 2) == 1) in.skipBytes(1);
while (in.readShort() == 0);
in.skipBytes(18);
if (in.getFilePointer() - fp > 123 && (fp % 2) == 0) {
in.seek(fp + 123);
}
int x = in.readInt();
int y = in.readInt();
int div = in.readShort();
x /= (div == 0 ? 1 : div);
div = in.readShort();
y /= (div == 0 ? 1 : div);
if (x > 16 && (x < sizeX[nextName - 1] ||
sizeX[nextName - 1] == 0) && y > 16 &&
(y < sizeY[nextName - 1] || sizeY[nextName - 1] == 0))
{
sizeX[nextName - 1] = x;
sizeY[nextName - 1] = y;
adjust = false;
}
in.seek(pointer + 214);
int validBits = in.readShort();
if (core[nextName - 1].bitsPerPixel == 0 && validBits <= 16) {
core[nextName - 1].bitsPerPixel = validBits;
}
}
}
else if (n == 'm') {
// this block should contain a channel name
if (in.getFilePointer() > pixelOffsets.get(0).longValue()) {
in.skipBytes(14);
channelNames.add(in.readCString().trim());
}
}
else if (n == 'd') {
// objective info and pixel size X/Y
in.skipBytes(6);
long fp = in.getFilePointer();
objective = in.readCString();
in.seek(fp + 144);
pixelSize.add(in.readFloat());
}
else if (n == 'e') {
in.skipBytes(174);
ndFilters.add(new Double(in.readFloat()));
in.skipBytes(40);
if (nextName < getSeriesCount()) {
setSeries(nextName);
addSeriesMeta("channel " + ndFilters.size() + " intensification",
in.readShort());
}
}
else if (n == 'k') {
in.skipBytes(14);
if (nextName > 0) setSeries(nextName - 1);
addSeriesMeta("Mag. changer", in.readCString());
}
else if (isSpool) {
// spool files don't necessarily have block identifiers
for (int j=0; j<pixelOffsets.size(); j++) {
long end = j == pixelOffsets.size() - 1 ? in.length() :
pixelOffsets.get(j + 1).longValue();
if (in.getFilePointer() < end) {
in.skipBytes(14);
int check = in.readShort();
int x = in.readShort();
int y = in.readShort();
if (check == 0 && x > 16 && y > 16) {
sizeX[j] = x;
sizeY[j] = y;
}
adjust = false;
break;
}
}
}
}
}
planeOffset = new long[getSeriesCount()][];
boolean divByTwo = false;
for (int i=0; i<getSeriesCount(); i++) {
setSeries(i);
Vector<Integer> pixelIndexes = uniqueSeries.get(orderedSeries.get(i));
int nBlocks = pixelIndexes.size();
int index = pixelIndexes.get(0);
long pixels = pixelLengths.get(index).longValue() / 2;
boolean x = true;
core[i].sizeX = sizeX[index];
core[i].sizeY = sizeY[index];
core[i].sizeC = sizeC[index];
core[i].sizeZ = sizeZ[index];
if (core[i].sizeZ % nBlocks == 0) {
core[i].sizeZ /= nBlocks;
}
if (divByTwo) core[i].sizeX /= 2;
if (getSizeC() == 0) core[i].sizeC = 1;
if (getSizeZ() == 0) core[i].sizeZ = 1;
long plane = pixels / (getSizeC() * getSizeZ());
if (getSizeX() * getSizeY() == pixels) {
if (getSizeC() == 2 && (getSizeX() % 2 == 0) && (getSizeY() % 2 == 0)) {
core[i].sizeX /= 2;
divByTwo = true;
}
else {
core[i].sizeC = 1;
}
core[i].sizeZ = 1;
}
else if (getSizeX() * getSizeY() * getSizeZ() == pixels) {
if (getSizeC() == 2 && (getSizeX() % 2 == 0) && (getSizeY() % 2 == 0)) {
core[i].sizeX /= 2;
divByTwo = true;
}
else {
core[i].sizeC = 1;
core[i].sizeZ = (int) (pixels / (getSizeX() * getSizeY()));
}
}
else if (getSizeX() * getSizeY() * getSizeC() == pixels) {
core[i].sizeC = (int) (pixels / (getSizeX() * getSizeY()));
core[i].sizeZ = 1;
}
else {
long p = pixels / (getSizeX() * getSizeY());
if (p * getSizeX() * getSizeY() == pixels &&
p != getSizeC() * getSizeZ())
{
if (p % getSizeC() != 0) {
core[i].sizeC = 1;
core[i].sizeZ = (int) p;
}
else core[i].sizeZ = (int) (p / getSizeC());
}
}
plane = pixels / (getSizeC() * getSizeZ());
long diff =
2 * (pixels - (getSizeX() * getSizeY() * getSizeC() * getSizeZ()));
if ((pixelLengths.get(index).longValue() % 2) == 1) {
diff++;
}
if (i == 0) {
diff = 0;
}
if (adjust && diff == 0) {
boolean widthGreater = getSizeX() > getSizeY();
while (getSizeX() * getSizeY() > plane) {
if (x) core[i].sizeX /= 2;
else core[i].sizeY /= 2;
x = !x;
}
while (getSizeX() * getSizeY() < plane ||
(getSizeX() < getSizeY() && widthGreater))
{
core[i].sizeX++;
core[i].sizeY = (int) (plane / getSizeX());
}
}
int nPlanes = getSizeZ() * getSizeC();
core[i].sizeT = (int) (pixels / (getSizeX() * getSizeY() * nPlanes));
while (getSizeX() * getSizeY() * nPlanes * getSizeT() > pixels) {
core[i].sizeT--;
}
if (getSizeT() == 0) core[i].sizeT = 1;
core[i].sizeT *= nBlocks;
core[i].imageCount = nPlanes * getSizeT();
core[i].pixelType = FormatTools.UINT16;
core[i].dimensionOrder = nBlocks > 1 ? "XYZCT" : "XYZTC";
core[i].indexed = false;
core[i].falseColor = false;
core[i].metadataComplete = true;
planeOffset[i] = new long[getImageCount()];
int nextImage = 0;
for (Integer pixelIndex : pixelIndexes) {
long offset = pixelOffsets.get(pixelIndex) + diff;
long length = pixelLengths.get(pixelIndex);
int planeSize = getSizeX() * getSizeY() * 2;
int planes = (int) (length / planeSize);
for (int p=0; p<planes; p++, nextImage++) {
if (nextImage < planeOffset[i].length) {
planeOffset[i][nextImage] = offset + p * planeSize;
}
}
}
}
setSeries(0);
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this);
// populate Image data
for (int i=0; i<getSeriesCount(); i++) {
if (imageNames[i] != null) store.setImageName(imageNames[i], i);
MetadataTools.setDefaultCreationDate(store, id, i);
}
if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
// link Instrument and Image
String instrumentID = MetadataTools.createLSID("Instrument", 0);
store.setInstrumentID(instrumentID, 0);
store.setImageInstrumentRef(instrumentID, 0);
int index = 0;
// populate Objective data
store.setObjectiveModel(objective, 0, 0);
store.setObjectiveCorrection(getCorrection("Other"), 0, 0);
store.setObjectiveImmersion(getImmersion("Other"), 0, 0);
// link Objective to Image
String objectiveID = MetadataTools.createLSID("Objective", 0, 0);
store.setObjectiveID(objectiveID, 0, 0);
store.setImageObjectiveSettingsID(objectiveID, 0);
// populate Dimensions data
for (int i=0; i<getSeriesCount(); i++) {
if (i < pixelSize.size()) {
Double size = new Double(pixelSize.get(i));
if (size > 0) {
store.setPixelsPhysicalSizeX(new PositiveFloat(size), i);
store.setPixelsPhysicalSizeY(new PositiveFloat(size), i);
}
}
int idx = 0;
for (int q=0; q<i; q++) {
idx += core[q].sizeC;
}
if (idx < pixelSizeZ.size() && pixelSizeZ.get(idx) != null) {
if (pixelSizeZ.get(idx) > 0) {
store.setPixelsPhysicalSizeZ(
new PositiveFloat(pixelSizeZ.get(idx)), i);
}
}
}
// populate LogicalChannel data
for (int i=0; i<getSeriesCount(); i++) {
setSeries(i);
for (int c=0; c<getSizeC(); c++) {
if (index < channelNames.size() && channelNames.get(index) != null) {
store.setChannelName(channelNames.get(index), i, c);
addSeriesMeta("channel " + c, channelNames.get(index));
}
if (index < ndFilters.size() && ndFilters.get(index) != null) {
store.setChannelNDFilter(ndFilters.get(index), i, c);
addSeriesMeta("channel " + c + " Neutral density",
ndFilters.get(index));
}
index++;
}
}
setSeries(0);
}
}
// -- Helper methods --
private boolean withinPixels(long offset) {
for (int i=0; i<pixelOffsets.size(); i++) {
long pixelOffset = pixelOffsets.get(i).longValue();
long pixelLength = pixelLengths.get(i).longValue();
if (offset >= pixelOffset && offset < (pixelOffset + pixelLength)) {
return true;
}
}
return false;
}
}