/*
* #%L
* OME Bio-Formats package for reading and converting biological file formats.
* %%
* Copyright (C) 2005 - 2015 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* 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, see
* <http://www.gnu.org/licenses/gpl-2.0.html>.
* #L%
*/
package loci.formats.in;
import loci.formats.in.BaseZeissReader;
import loci.formats.in.BaseZeissReader.Charset;
import loci.formats.in.BaseZeissReader.FeatureType;
import loci.common.DataTools;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.Stack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
/**
* SAX handler for parsing XML in Zeiss TIFF files.
*
* @author Roger Leigh <r.leigh at dundee.ac.uk>
*/
public class ZeissTIFFHandler extends DefaultHandler {
// -- Constants --
private static final Logger LOGGER =
LoggerFactory.getLogger(ZeissTIFFHandler.class);
// -- Fields --
// Reader
BaseZeissReader reader;
// Stack of XML elements to keep track of placement in the tree.
private Stack<String> nameStack = new Stack<String>();
// CDATA text stored while parsing. Note that this is limited to a
// single span between two tags, and CDATA with embedded elements is
// not supported (and not present in the Zeiss TIFF format).
private String cdata = new String();
// Main metadata tags for the image.
public TagSet main_tagset = new TagSet();
// Per-plane metadata for the image.
public ArrayList<Plane> planes = new ArrayList<Plane>();
// Scaling metadata for the image.
public ArrayList<Scaling> scalings = new ArrayList<Scaling>();
// Layer annotations (contain Shapes).
public ArrayList<BaseZeissReader.Layer> layers = new ArrayList<BaseZeissReader.Layer>();
// Current tags (during parsing).
private TagSet current_tagset;
// Current tag (during parsing).
private BaseZeissReader.Tag current_tag;
// Current scaling (during parsing).
private Scaling current_scaling;
// Current layer (during parsing).
private BaseZeissReader.Layer current_layer;
// Current shape (during parsing).
private BaseZeissReader.Shape current_shape;
// Found planes (during parsing).
private Set<String> planeNames = new HashSet<String>();
// Number of tags in current Tags block
int tag_count;
// Number of layers in current Layers block
int layer_count;
// Number of shapes in current Shapes block
int shape_count;
// -- ZeissTIFFHandler API methods --
ZeissTIFFHandler(ZeissTIFFReader reader) {
this.reader = reader;
}
@Override
public String toString()
{
String s = new String("TIFF-XML parsing\n");
s += main_tagset;
s += '\n';
for (Scaling sc : scalings ) {
s += sc;
s += '\n';
}
for (Plane p : planes ) {
s += p;
s += '\n';
}
return s;
}
// -- DefaultHandler API methods --
@Override
public void endElement(String uri,
String localName,
String qName) {
if (!nameStack.empty() && nameStack.peek().equals(qName))
nameStack.pop();
if (qName.equals("ROOT")) {
// Finalise data.
}
else if (qName.equals("Layers")) {
// No nothing; already handled.
}
else if (qName.equals("Shapes")) {
// No nothing; already handled.
}
else if (qName.startsWith("Item")) {
// Inside a Layer or Shape annotation. Determine which it is.
if (nameStack.peek().equals("Layers")) {
if (current_layer != null)
layers.add(current_layer);
current_layer = null;
}
else if (nameStack.peek().equals("Shapes")) {
if (current_shape != null)
current_layer.shapes.add(current_shape);
current_shape = null;
}
else
LOGGER.info("Parse error: tag found out of place: {}", qName);
}
else if (qName.equals("Scaling")) {
// Scaling metadata. __Version, Key, Category, Factor_n,
// Type_n, Unit_n, Origin_n, Angle_n and Matrix_n tags are
// valid here.
if (current_scaling != null)
scalings.add(current_scaling);
current_scaling = null;
}
else if (qName.equals("Key")) {
if (current_scaling != null) {
if (current_scaling.key == null)
current_scaling.key = cdata;
else
LOGGER.debug("Key already set");
}
}
else if (qName.equals("Category")) {
if (current_scaling != null) {
if (current_scaling.category == null)
current_scaling.category = Integer.parseInt(cdata);
else
LOGGER.debug("Category already set");
}
}
else if (qName.startsWith("Factor_")) {
Scaling.Dimension d = current_scaling.getDimension(qName);
d.factor = parseDouble(cdata);
}
else if (qName.startsWith("Type_")) {
Scaling.Dimension d = current_scaling.getDimension(qName);
d.type = Integer.parseInt(cdata);
}
else if (qName.startsWith("Unit_")) {
Scaling.Dimension d = current_scaling.getDimension(qName);
try {
d.unit = Integer.parseInt(cdata);
}
catch (Exception e) {
}
}
else if (qName.startsWith("Origin_")) {
Scaling.Dimension d = current_scaling.getDimension(qName);
d.origin = parseDouble(cdata);
}
else if (qName.startsWith("Angle_")) {
Scaling.Dimension d = current_scaling.getDimension(qName);
d.angle = parseDouble(cdata);
}
else if (qName.startsWith("Matrix_")) {
String value = qName.substring(qName.indexOf("_") + 1);
Integer index = Integer.parseInt(value);
Double mval = parseDouble(cdata);
current_scaling.matrix.put(index, mval);
}
else if (qName.equals("Tags")) {
// Save tags.
if (!nameStack.empty())
{
String parent = nameStack.peek();
if (parent.equals("ROOT"))
{
main_tagset = current_tagset;
current_tagset = null;
}
else if (parent.equals("Scaling"))
{
current_scaling.tagset = current_tagset;
current_tagset = null;
}
else if (nameStack.size() == 2) // Plane at top-level
{
Plane plane = new Plane(parent, current_tagset);
current_tagset = null;
planes.add(plane);
planeNames.add(plane.basename);
}
}
}
else if (qName.equals("__Version")) {
// Do nothing.
}
else if (qName.equals("AttributeShape")) {
// Found in Layers. Unknown purpose (empty in samples). Do nothing.
}
else if (qName.equals("SourceName")) {
// Found in Shapes Itemm. Unknown purpose (empty in samples). Do nothing.
}
else if (qName.equals("PredefinedStrings")) {
// Found in Shapes Itemm. Unknown purpose (empty in samples). Do nothing.
}
else if (qName.equals("Dummy")) {
// Found in Shapes Itemm. Unknown purpose (empty in samples). Do nothing.
}
else if (qName.equals("Features")) {
// Found in Shapes Itemm. Measurement features to record in the analysis results data table. Do nothing. Probably should be preserved in metadata.
}
else if (qName.equals("DrawFeatures")) {
// Found in Shapes Itemm. This is a measurement feature to display as part of the measurement overlay (in the Text element). Do nothing. Probably should be preserved in metadata.
// Measurements only.
}
else if (qName.equals("InputMethod")) {
// Found in Shapes Itemm. Unknown purpose (empty in samples). Do nothing.
}
else if (qName.equals("HandleSize2")) {
// Found in Shapes Itemm. Unknown purpose; probably UI hint, but no observed effect.
current_shape.handleSize = Integer.parseInt(cdata);
}
else if (qName.equals("Text")) {
current_shape.text = cdata;
//String t = new String("Test: (µm), text=" + cdata);
//System.out.println(t);
}
else if (qName.equals("PointCount")) {
current_shape.pointCount = Integer.parseInt(cdata);
}
else if (qName.equals("Points")) {
cdata = cdata.replaceAll("\\p{Cntrl}|\\p{Space}", "");
String[] numbers = cdata.split(",");
byte[] raw = new byte[numbers.length];
for (int i = 0; i < raw.length; i++) {
raw[i] = (byte) Integer.parseInt(numbers[i]);
}
current_shape.points = new double[current_shape.pointCount*2];
for (int i = 0; i < current_shape.pointCount; i++) {
current_shape.points[(i*2)] = DataTools.bytesToDouble(raw, (i*2)*8, true);
current_shape.points[(i*2)+1] = DataTools.bytesToDouble(raw, ((i*2)+1)*8, true);
}
}
else if (qName.equals("FontName")) {
current_shape.fontName = cdata;
}
else if (qName.equals("Name")) {
current_shape.name = cdata;
}
else if (qName.equals("SourceTagId")) {
int value = Integer.parseInt(cdata);
current_shape.tagID = reader.new Tag(value, BaseZeissReader.Context.MAIN);
}
else if (qName.equals("ShapeAttributes")) {
// Found in Shapes Itemm. This appears to be a horror of the first
// order, a comma separated list of 8-bit numbers, which appears to
// be a dump of a raw structure including doubles, uint32 integers
// and enums, plus padding. Attempt to convert the "serialised"
// data back into something usable. Initially remove all
// non-printing characters from the cdata.
cdata = cdata.replaceAll("\\p{Cntrl}|\\p{Space}", "");
//cdata.replaceAll("[[:cntrl:]]", "");
String[] numbers = cdata.split(",");
byte[] raw = new byte[numbers.length];
for (int i = 0; i < raw.length; i++) {
raw[i] = (byte) Integer.parseInt(numbers[i]);
}
if (raw.length >= 152) {
// Note that all coordinates have the origin in the upper left corner.
// We only have examples of packed structures of 156 bytes.
int isize = DataTools.bytesToInt(raw, 0, true);
if (raw.length < isize)
LOGGER.info("ShapeAttributes length ({}) is less than internal size ({})! Trying to continue...", raw.length, isize);
int type = DataTools.bytesToInt(raw, 4, true);
// Annotation feature type.
current_shape.type = FeatureType.get(type);
current_shape.unknown2 = DataTools.bytesToInt(raw, 8, true);
current_shape.unknown3 = DataTools.bytesToInt(raw, 12, true);
current_shape.x1 = DataTools.bytesToInt(raw, 16, true);
current_shape.y1 = DataTools.bytesToInt(raw, 20, true);
current_shape.x2 = DataTools.bytesToInt(raw, 24, true);
current_shape.y2 = DataTools.bytesToInt(raw, 28, true);
current_shape.width = current_shape.x2 - current_shape.x1;
current_shape.height = current_shape.y2 - current_shape.y1;
current_shape.unknown4 = DataTools.bytesToInt(raw, 32, true);
current_shape.unknown5 = DataTools.bytesToInt(raw, 36, true);
current_shape.unknown6 = parseColor(raw[40], raw[41], raw[42]);
current_shape.unknown7 = DataTools.bytesToInt(raw, 44, true);
current_shape.fillColour = parseColor(raw[48], raw[49], raw[50]); // We don't include alpha from the file.
current_shape.textColour = parseColor(raw[52], raw[53], raw[54]); // It's not clear that it's set to a
current_shape.drawColour = parseColor(raw[56], raw[57], raw[58]); // sensible value. (Not exposed in software.)
current_shape.lineWidth = DataTools.bytesToInt(raw, 60, true);
current_shape.drawStyle = BaseZeissReader.DrawStyle.get(DataTools.bytesToInt(raw, 64, true));
current_shape.fillStyle = BaseZeissReader.FillStyle.get(DataTools.bytesToInt(raw, 68, true));
current_shape.unknown8 = DataTools.bytesToInt(raw, 72, true);
current_shape.strikeout = (DataTools.bytesToInt(raw, 76, true) != 0);
// Windows TrueType font weighting.
current_shape.fontWeight = DataTools.bytesToInt(raw, 80, true);
current_shape.bold = (current_shape.fontWeight >= 600);
current_shape.fontSize = DataTools.bytesToInt(raw, 84, true);
current_shape.italic = (DataTools.bytesToInt(raw, 88, true) != 0);
current_shape.underline = (DataTools.bytesToInt(raw, 92, true) != 0);
current_shape.textAlignment = BaseZeissReader.TextAlignment.get(DataTools.bytesToInt(raw, 96, true));
current_shape.unknown10 = DataTools.bytesToInt(raw, 100, true);
current_shape.unknown11 = DataTools.bytesToInt(raw, 104, true);
current_shape.unknown12 = DataTools.bytesToInt(raw, 108, true);
current_shape.unknown13 = DataTools.bytesToInt(raw, 112, true);
current_shape.unknown14 = DataTools.bytesToInt(raw, 116, true);
current_shape.unknown15 = DataTools.bytesToInt(raw, 120, true);
current_shape.unknown16 = DataTools.bytesToInt(raw, 124, true);
current_shape.unknown17 = DataTools.bytesToInt(raw, 128, true);
current_shape.unknown18 = DataTools.bytesToInt(raw, 132, true);
current_shape.displayTag = (DataTools.bytesToInt(raw, 148, true) != 0); // FF00==1 0000==0.
//current_shape.zpos = (DataTools.bytesToInt(raw, 104, true));
current_shape.lineEndStyle = BaseZeissReader.LineEndStyle.get(DataTools.bytesToInt(raw, 136, true));
current_shape.pointStyle = BaseZeissReader.PointStyle.get(DataTools.bytesToInt(raw, 136, true));
current_shape.lineEndSize = DataTools.bytesToInt(raw, 140, true);
current_shape.lineEndPositions = BaseZeissReader.LineEndPositions.get(DataTools.bytesToInt(raw, 144, true));
if (isize >= 156)
current_shape.charset = Charset.get(DataTools.bytesToInt(raw, 152, true));
}
}
else if (qName.equals("Flags")) {
if (qName.startsWith("Item")) {
// Found in Itemn of Layers. Unknown purpose (set to 1 in samples). Do nothing.
}
}
else if (qName.equals("Count")) {
// Inside a Tag, Layer or Shape annotation. Determine which it is.
if (nameStack.peek().equals("Tags")) {
this.tag_count = Integer.parseInt(cdata);
current_tagset.count = Integer.parseInt(cdata); // TODO: Remove
}
else if (nameStack.peek().equals("Layers"))
this.layer_count = Integer.parseInt(cdata);
else if (nameStack.peek().equals("Shapes"))
this.shape_count = Integer.parseInt(cdata);
else
LOGGER.info("Parse error: tag found out of place: {}", qName);
}
else if (qName.equals("Key")) {
// Inside a Layers or Itemn (Layer) or Itemm (Shape) annotation. Determine which it is.
if (nameStack.peek().equals("Layers")) {
// Unknown purpose. Empty in sample files.
}
if (nameStack.peek().equals("Shapes")) {
// Unknown purpose. Set to "Default" in sample files.
}
else if (nameStack.peek().startsWith("Item")) {
// See if we're in a Layers or a Shapes container
int stackSize = nameStack.size();
if (stackSize >= 2) {
if (nameStack.get(stackSize -2).equals("Layers"))
current_layer.key = Integer.parseInt(cdata);
else if (nameStack.get(stackSize -2).equals("Shapes"))
current_layer.key = Integer.parseInt(cdata);
}
}
else
LOGGER.info("Parse error: tag found out of place: {}", qName);
}
else if (qName.equals("Class")) {
// AxioVision-specific CLSID? Ignore.
}
else if (!nameStack.empty() && nameStack.peek().equals("Tags"))
{
String type = qName.substring(0,1);
// Only process Ann, Inn and Vnn elements. No other known
// elements are found in a Tags block other than Count, so
// ignore them.
if (type.equals("A") || type.equals("I") || type.equals("V"))
{
String value = qName.substring(1);
int index = Integer.parseInt(value);
// A tags block should always be at least one element deep, so we should have a minimum of two elements on the stack.
int stackSize = nameStack.size();
BaseZeissReader.Context context = BaseZeissReader.Context.PLANE;
if (stackSize >= 2)
{
if (nameStack.get(stackSize - 2).equals("ROOT"))
context = BaseZeissReader.Context.MAIN;
else if (nameStack.get(stackSize - 2).equals("Scaling"))
context = BaseZeissReader.Context.SCALING;
}
// This checks and copes with index mismatches if the XML is bad.
if (current_tag == null || current_tag.getIndex() != index)
current_tag = reader.new Tag(index, context);
// If the index is out of range, ignore the tag. It will remain invalid.
// Note that the index counts from zero, while the total is a count.
if (current_tagset.found >= current_tagset.count)
LOGGER.info("Found more tags then declared");
if (type.equals("V")) // Set to null if empty/unset?
current_tag.setValue(cdata);
else if (type.equals("I")) // Skip if unset.
current_tag.setKey(Integer.parseInt(cdata));
else if (type.equals("A")) // Set to 0 if unset...
current_tag.setCategory(Integer.parseInt(cdata));
else
LOGGER.info("Unknown tag: {}", qName);
if (current_tag.valid()) {
current_tagset.tags.add(current_tag);
current_tagset.found++;
}
}
else
{
LOGGER.info("Unknown tag: {}", qName);
}
}
else
{
// Presumably, this is a plane. To verify that, we need to
// check for the presence of an embedded Tags block, and then
// for the presence of the appropriate plane-specific tags.
// And, additionally, that the tag-based filename prefix
// matches an existing file on disc.
if (!planeNames.contains(qName))
LOGGER.info("Unknown tag: {}", qName);
}
cdata = "";
}
@Override
public void characters(char[] ch,
int start,
int length)
{
String s = new String(ch, start, length);
cdata += s;
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes)
{
cdata = "";
if (qName.equals("Scaling")) {
// Scaling metadata. __Version, Key, Category, Factor_n,
// Type_n, Unit_n, Origin_n, Angle_n and Matrix_n tags are
// valid here.
current_scaling = new Scaling();
}
else if (qName.equals("Tags")) {
// Start of metadata block. __Version, Count, Vn, In and An
// tags are valid here.
current_tagset = new TagSet();
}
else if (qName.startsWith("Item")) {
// Start of a Layer or Shape annotation. Determine which it is.
if (nameStack.peek().equals("Layers"))
current_layer = reader.new Layer();
else if (nameStack.peek().equals("Shapes"))
current_shape = reader.new Shape();
else
LOGGER.info("Parse error: tag found out of place: {}", qName);
}
else if (qName.equals("Class")) {
// AxioVision-specific CLSID. Ignore.
}
else {
// Other or unknown tag; will be handled by endElement.
}
nameStack.push(qName);
}
// -- Helper classes and functions --
/**
* Parse a Double from a String. Unlike the standard method, this one replaces commas with decimal points (if present).
* @param number the number to parse
* @return a Double. 0 if number was null.
*/
private static double parseDouble(String number) {
if (number != null) {
number = number.replaceAll(",", ".");
return Double.parseDouble(number);
}
return 0;
}
/**
* Content of a single tag from a Tags block.
*/
class Tag {
// index number of the tag in the XML. Not useful except for parsing validation.
public int index;
// key number of the tag (I element). Needs mapping to a descriptive name.
public int key;
// value of the tag (V element).
public String value;
// category (presumed) of the tag (A element).
public int category;
/**
* Constructor.
* All variables are initialised to be invalid to permit later validation of correct parsing.
* @param index the index number of the tag.
*/
Tag (int index)
{
this.index = index;
key = -1;
value = null;
category = -1;
}
/**
* Check if the tag is valid (key, value and category have been set).
* @return true if valid, otherwise false.
*/
public boolean valid () {
return key != -1 && value != null && category != -1;
}
@Override
public String toString() {
return new String(" T: K=" + key +
" V=" + value + " C=" + category +
" I=" + index);
}
}
/**
* A collection of tags from a single Tags block.
*/
class TagSet
{
// Number of tags. Used only for validation
int count = 0;
int found = 0;
// Mapping between tag key number and tag.
public ArrayList<BaseZeissReader.Tag> tags = new ArrayList<BaseZeissReader.Tag>();
@Override
public String toString() {
String s = new String(" Tags(" + count + "):\n");
for (BaseZeissReader.Tag t : tags) {
s += t;
s += '\n';
}
return s;
}
}
/**
* Metadata for a single image plane.
*/
public class Plane
{
// Name of the plane.
public String basename;
// Tags associated with the plane.
public TagSet tagset;
/**
* Constructor
* @param basename the name of the plane.
* @param tagset the tags for the plane.
*/
Plane(String basename, TagSet tagset) {
this.basename = basename;
this.tagset = tagset;
}
@Override
public String toString() {
String s = new String(" Plane: " + basename + '\n');
s += tagset;
s += '\n';
return s;
}
}
/**
* Scaling metadata.
*/
class Scaling
{
// Key name for this scaling.
String key;
// Category for this scaling (numeric).
Integer category;
// Mapping between dimension number and dimension object.
Map<Integer, Dimension> dims = new HashMap<Integer, Dimension>();
// Matrix. Purpose unknown.
Map<Integer,Double> matrix = new HashMap<Integer,Double>();
// Metadata associated with this scaling.
TagSet tagset;
/**
* Get a dimension by its index. If the dimension does not yet
* exist, a new one will be created.
* @param key An XML element name. The index number will be parsed
* from this name.
* @return A Dimension object.
*/
Dimension
getDimension(String key)
{
String value = key.substring(key.indexOf("_") + 1);
Integer index = Integer.parseInt(value);
Dimension d = dims.get(index);
if (d == null)
{
d = new Dimension(index);
dims.put(index, d);
}
return d;
}
@Override
public String toString() {
String s = new String("Scaling\n");
s += " Key=" + key + "\n";
s += " Cat=" + category + "\n";
List<Integer> dimarray = new LinkedList<Integer>(dims.keySet());
Collections.sort(dimarray);
for (Integer dim : dimarray)
{
Dimension d = dims.get(dim);
s += " Dim" + dim + "=\n"
+ " Ftr=" + d.factor + "\n"
+ " Typ=" + d.type + "\n"
+ " Unt=" + d.unit + "\n"
+ " Org=" + d.origin + "\n"
+ " Ang=" + d.angle + "\n";
}
s += tagset;
s+= '\n';
return s;
}
/**
* Scaling information for a single dimension.
*/
class Dimension
{
// Dimension number (012) == (xyz)?
Integer dimension;
// Scale factor.
Double factor;
// ?
Integer type;
// Unit type.
// 0 = No scale (pixel)
// 72 = meter, 72=decimeter (spec bogus--duplicate value), micrometer=76, nanometer=77.
// inch=81; mil=84 (documented as micrometres, but a mil is 1/1000")
// TIME:
// second=136, millisecond=139, microsecond=140, minute=145, hour=146,
Integer unit;
// Origin (of scale bar or measurement?)
Double origin;
// Angle (of scale bar or measurement?)
Double angle;
/**
* Constructor.
* @param dimension The dimension number.
*/
Dimension(Integer dimension)
{
this.dimension = dimension;
}
}
}
// TODO: Replace me with a proper Color class when available.
protected static int parseColor(byte r, byte g, byte b) {
return ((r&0xFF) << 24) | ((g&0xFF) << 16) | ((b&0xFF) << 8);
}
}