/* * #%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 java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import loci.common.DataTools; import loci.common.Location; import loci.common.RandomAccessInputStream; import loci.common.services.DependencyException; import loci.common.services.ServiceFactory; import loci.formats.FormatException; import loci.formats.FormatTools; import loci.formats.codec.CodecOptions; import loci.formats.codec.JPEGCodec; import loci.formats.codec.ZlibCodec; import loci.formats.meta.DummyMetadata; import loci.formats.meta.MetadataStore; import loci.formats.services.POIService; /** * ZeissZVIReader is the file format reader for Zeiss ZVI files. * * @author Melissa Linkert melissa at glencoesoftware.com */ public class ZeissZVIReader extends BaseZeissReader { // -- Constants -- public static final int ZVI_MAGIC_BYTES = 0xd0cf11e0; private static final long ROI_SIGNATURE = 0x21fff6977547000dL; // -- Fields -- protected transient POIService poi; protected String[] files; // -- Constructor -- /** Constructs a new ZeissZVI reader. */ public ZeissZVIReader() { super("Zeiss Vision Image (ZVI)", "zvi"); domains = new String[] {FormatTools.LM_DOMAIN}; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { final int blockLen = 65536; if (!FormatTools.validStream(stream, blockLen, false)) return false; int magic = stream.readInt(); if (magic != ZVI_MAGIC_BYTES) return false; return true; } /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) */ @Override 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); lastPlane = no; if (poi == null) { initPOIService(); } int bytes = FormatTools.getBytesPerPixel(getPixelType()); int pixel = bytes * getRGBChannelCount(); CodecOptions options = new CodecOptions(); options.littleEndian = isLittleEndian(); options.interleaved = isInterleaved(); int index = no; if (getSeriesCount() == 1) { int[] coords = getZCTCoords(no); for (int q=0; q<coordinates.length; q++) { if (coordinates[q][0] == coords[0] && coordinates[q][1] == coords[1] && coordinates[q][2] == coords[2]) { index = q; break; } } } else { index += getSeries() * getImageCount(); } if (index >= imageFiles.length) { return buf; } RandomAccessInputStream s = poi.getDocumentStream(imageFiles[index]); s.seek(offsets[index]); int len = w * pixel; int row = getSizeX() * pixel; if (isJPEG) { byte[] t = new JPEGCodec().decompress(s, options); for (int yy=0; yy<h; yy++) { System.arraycopy(t, (yy + y) * row + x * pixel, buf, yy*len, len); } } else if (isZlib) { byte[] t = new ZlibCodec().decompress(s, options); for (int yy=0; yy<h; yy++) { int src = (yy + y) * row + x * pixel; int dest = yy * len; if (src + len <= t.length && dest + len <= buf.length) { System.arraycopy(t, src, buf, dest, len); } else break; } } else { readPlane(s, x, y, w, h, buf); } s.close(); if (isRGB() && !isJPEG) { // reverse bytes in groups of 3 to account for BGR storage byte[] bb = new byte[bytes]; for (int i=0; i<buf.length; i+=bpp) { System.arraycopy(buf, i + 2*bytes, bb, 0, bytes); System.arraycopy(buf, i, buf, i + 2*bytes, bytes); System.arraycopy(bb, 0, buf, i, bytes); } } return buf; } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (poi != null) poi.close(); poi = null; files = null; } // -- Internal FormatReader API methods -- @Override protected void initFile(String id) throws FormatException, IOException { super.initFile(id); super.initFileMain(id); // double-check that the coordinates are valid // all of the image numbers must be accounted for HashMap<Integer, Boolean> valid = new HashMap<Integer, Boolean>(); for (int i=0; i<coordinates.length; i++) { valid.put(i, false); } for (int i=0; i<coordinates.length; i++) { try { int index = getIndex(coordinates[i][0], coordinates[i][1], coordinates[i][2]); valid.put(index, true); } catch (IllegalArgumentException e) { LOGGER.trace("Found invalid coordinates", e); } } if (valid.containsValue(false)) { coordinates = new int[0][0]; } } @Override protected void initVars(String id) throws FormatException, IOException { super.initVars(id); initPOIService(); countImages(); } private void initPOIService() throws FormatException, IOException { try { ServiceFactory factory = new ServiceFactory(); poi = factory.getInstance(POIService.class); } catch (DependencyException de) { throw new FormatException("POI library not found", de); } poi.initialize(Location.getMappedId(getCurrentFile())); } /* @see loci.formats.FormatReader#initFile(String) */ @Override protected void fillMetadataPass1(MetadataStore store) throws FormatException, IOException { super.fillMetadataPass1(store); // parse each embedded file for (String name : files) { String relPath = name.substring(name.lastIndexOf(File.separator) + 1); if (!relPath.toUpperCase().equals("CONTENTS")) continue; String dirName = name.substring(0, name.lastIndexOf(File.separator)); if (dirName.indexOf(File.separator) != -1) { dirName = dirName.substring(dirName.lastIndexOf(File.separator) + 1); } if (name.indexOf("Scaling") == -1 && dirName.equals("Tags")) { int imageNum = getImageNumber(name, -1); if (imageNum == -1) { parseTags(imageNum, name, new DummyMetadata()); } else tagsToParse.add(name); } else if (dirName.equals("Shapes") && name.indexOf("Item") != -1) { int imageNum = getImageNumber(name, -1); if (imageNum != -1) { try { parseROIs(imageNum, name, store); } catch (IOException e) { LOGGER.debug("Could not parse all ROIs.", e); } } } else if (dirName.equals("Image") || dirName.toUpperCase().indexOf("ITEM") != -1) { int imageNum = getImageNumber(dirName, getImageCount() == 1 ? 0 : -1); if (imageNum == -1) continue; // found a valid image stream RandomAccessInputStream s = poi.getDocumentStream(name); s.order(true); if (s.length() <= 1024) { s.close(); continue; } for (int q=0; q<11; q++) { getNextTag(s); } s.skipBytes(2); int len = s.readInt() - 20; s.skipBytes(8); int zidx = s.readInt(); int cidx = s.readInt(); int tidx = s.readInt(); zIndices.add(zidx); timepointIndices.add(tidx); channelIndices.add(cidx); s.skipBytes(len); for (int q=0; q<5; q++) { getNextTag(s); } s.skipBytes(4); //if (getSizeX() == 0) { core.get(0).sizeX = s.readInt(); core.get(0).sizeY = s.readInt(); //} //else s.skipBytes(8); s.skipBytes(4); if (bpp == 0) { bpp = s.readInt(); } else s.skipBytes(4); s.skipBytes(4); int valid = s.readInt(); String check = s.readString(4).trim(); isZlib = (valid == 0 || valid == 1) && check.equals("WZL"); isJPEG = (valid == 0 || valid == 1) && !isZlib; // save the offset to the pixel data offsets[imageNum] = (int) s.getFilePointer() - 4; if (isZlib) offsets[imageNum] += 8; coordinates[imageNum][0] = zidx; coordinates[imageNum][1] = cidx; coordinates[imageNum][2] = tidx; imageFiles[imageNum] = name; s.close(); } } } @Override protected void fillMetadataPass3(MetadataStore store) throws FormatException, IOException { super.fillMetadataPass3(store); // calculate tile dimensions and number of tiles if (core.size() > 1) { Integer[] t = tiles.keySet().toArray(new Integer[tiles.size()]); Arrays.sort(t); final List<Integer> tmpOffsets = new ArrayList<Integer>(); final List<String> tmpFiles = new ArrayList<String>(); int index = 0; for (Integer key : t) { int nTiles = tiles.get(key).intValue(); if (nTiles < getImageCount()) { tiles.remove(key); } else { for (int p=0; p<nTiles; p++) { tmpOffsets.add(offsets[index + p]); tmpFiles.add(imageFiles[index + p]); } } index += nTiles; } offsets = new int[tmpOffsets.size()]; for (int i=0; i<offsets.length; i++) { offsets[i] = tmpOffsets.get(i).intValue(); } imageFiles = tmpFiles.toArray(new String[tmpFiles.size()]); } } @Override protected void fillMetadataPass5(MetadataStore store) throws FormatException, IOException { super.fillMetadataPass5(store); for (String name : tagsToParse) { int imageNum = getImageNumber(name, -1); parseTags(imageNum, name, store); } } @Override protected void countImages() { // count number of images files = (String[]) poi.getDocumentList().toArray(new String[0]); Arrays.sort(files, new Comparator<String>() { @Override public int compare(String o1, String o2) { final Integer n1 = getImageNumber(o1, -1); final Integer n2 = getImageNumber(o2, -1); return n1.compareTo(n2); } }); core.get(0).imageCount = 0; for (String file : files) { String uname = file.toUpperCase(); uname = uname.substring(uname.indexOf(File.separator) + 1); if (uname.endsWith("CONTENTS") && (uname.startsWith("IMAGE") || uname.indexOf("ITEM") != -1) && poi.getFileSize(file) > 1024) { int imageNumber = getImageNumber(file, 0); if (imageNumber >= getImageCount()) { core.get(0).imageCount++; } } } super.countImages(); } private int getImageNumber(String dirName, int defaultNumber) { if (dirName.toUpperCase().indexOf("ITEM") != -1) { int open = dirName.indexOf("("); int close = dirName.indexOf(")"); if (open < 0 || close < 0 || close < open) return defaultNumber; return Integer.parseInt(dirName.substring(open + 1, close)); } return defaultNumber; } private String getNextTag(RandomAccessInputStream s) throws IOException { int type = s.readShort(); switch (type) { case 0: case 1: return ""; case 2: return String.valueOf(s.readShort()); case 3: case 22: case 23: return String.valueOf(s.readInt()); case 4: return String.valueOf(s.readFloat()); case 5: return String.valueOf(s.readDouble()); case 7: case 20: case 21: return String.valueOf(s.readLong()); case 8: case 69: int len = s.readInt(); return s.readString(len); case 9: case 13: s.skipBytes(16); return ""; case 11: return String.valueOf(s.readShort()!=0); case 63: case 65: len = s.readInt(); s.skipBytes(len); return ""; case 66: len = s.readShort(); return s.readString(len); default: long old = s.getFilePointer(); while (s.readShort() != 3 && s.getFilePointer() + 2 < s.length()); long fp = s.getFilePointer() - 2; s.seek(old - 2); return s.readString((int) (fp - old + 2)); } } /** Parse all of the tags in a stream. */ private void parseTags(int image, String file, MetadataStore store) throws FormatException, IOException { ArrayList<Tag> tags = new ArrayList<Tag>(); RandomAccessInputStream s = poi.getDocumentStream(file); s.order(true); s.seek(8); int count = s.readInt(); for (int i=0; i<count; i++) { if (s.getFilePointer() + 2 >= s.length()) break; String value = DataTools.stripString(getNextTag(s)); s.skipBytes(2); int tagID = s.readInt(); s.skipBytes(6); tags.add(new Tag(tagID, value, Context.MAIN)); } parseMainTags(image, store, tags); s.close(); } /** * Parse ROI data from the given RandomAccessInputStream and store it in the * given MetadataStore. */ private void parseROIs(int imageNum, String name, MetadataStore store) throws IOException { MetadataLevel level = getMetadataOptions().getMetadataLevel(); if (level == MetadataLevel.MINIMUM || level == MetadataLevel.NO_OVERLAYS) { return; } RandomAccessInputStream s = poi.getDocumentStream(name); s.setEncoding("UTF-16LE"); s.order(true); // scan stream for offsets to each ROI final List<Long> roiOffsets = new ArrayList<Long>(); // Bytes 0x0-1 == 0x3 // Bytes 0x2-5 == Layer version (04100010) // Byte 0x10 == Shape count s.seek(2); int layerversion = s.readInt(); LOGGER.debug("LAYER@{} version={}", s.getFilePointer()-4, layerversion); // Layer name (assumed). This is usually NULL in most files, so // we need to explicitly check whether it's a string prior to // parsing it. The only file seen with a non-null string here did // not contain any shapes in the layer (purpose of the empty layer // unknown). String layername = null; { long tmp = s.getFilePointer(); if (s.readShort() == 8) { s.seek(tmp); layername = parseROIString(s); } if (layername != null) LOGGER.debug(" Name={}", layername); else LOGGER.debug(" Name=NULL"); } s.skipBytes(8); int roiCount = s.readShort(); int roiFound = 0; LOGGER.debug(" ShapeCount@{} count={}", s.getFilePointer()-2, roiCount); // Add new layer for this set of shapes. Layer nlayer = new Layer(); nlayer.name = layername; layers.add(nlayer); // Following signature (from sig start): // Bytes 0x12-15 == Shape version (0B200010) // Bytes 0x28-nn == Shape attributes (size is first short) while (roiFound < roiCount && s.getFilePointer() < s.length() - 8) { // find next ROI signature long signature = s.readLong() & 0xffffffffffffffffL; while (signature != ROI_SIGNATURE) { if (s.getFilePointer() >= s.length()) break; s.seek(s.getFilePointer() - 6); signature = s.readLong() & 0xffffffffffffffffL; } if (s.getFilePointer() >= s.length()) { break; } long roiOffset = s.getFilePointer() - 8; roiOffsets.add(roiOffset); LOGGER.debug("ROI@{}", roiOffset); // Found ROI; now fill out the shape details and add to the // layer. s.seek(roiOffset + 26); int length = s.readInt(); Shape nshape = new Shape(); s.skipBytes(length + 6); long shapeAttrOffset = s.getFilePointer(); int shapeAttrLength = s.readInt(); nshape.type = FeatureType.get(s.readInt()); LOGGER.debug(" ShapeAttrs@{} len={}", shapeAttrOffset, shapeAttrLength); if (shapeAttrLength < 32) // Broken attrs. break; // read the bounding box s.skipBytes(8); nshape.x1 = s.readInt(); nshape.y1 = s.readInt(); nshape.x2 = s.readInt(); nshape.y2 = s.readInt(); nshape.width = nshape.x2 - nshape.x1; nshape.height = nshape.y2 - nshape.y1; LOGGER.debug(" Bounding Box"); if (shapeAttrLength >= 72) { // Basic shape styling s.skipBytes(16); nshape.fillColour = s.readInt(); nshape.textColour = s.readInt(); nshape.drawColour = s.readInt(); nshape.lineWidth = s.readInt(); nshape.drawStyle = DrawStyle.get(s.readInt()); nshape.fillStyle = FillStyle.get(s.readInt()); nshape.strikeout = (s.readInt() != 0); LOGGER.debug(" Shape styles"); } if (shapeAttrLength >= 100) { // Font styles // Windows TrueType font weighting. nshape.fontWeight = s.readInt(); nshape.bold = (nshape.fontWeight >= 600); nshape.fontSize = s.readInt(); nshape.italic = (s.readInt() != 0); nshape.underline = (s.readInt() != 0); nshape.textAlignment = TextAlignment.get(s.readInt()); LOGGER.debug(" Font styles"); } if (shapeAttrLength >= 148) { // Line styles s.skipBytes(36); nshape.lineEndStyle = BaseZeissReader.LineEndStyle.get(s.readInt()); nshape.pointStyle = BaseZeissReader.PointStyle.get(s.readInt()); nshape.lineEndSize = s.readInt(); nshape.lineEndPositions = BaseZeissReader.LineEndPositions.get(s.readInt()); LOGGER.debug(" Line styles"); } if (shapeAttrLength >= 152) { nshape.displayTag = (s.readInt() != 0); LOGGER.debug(" Tag display"); } if (shapeAttrLength >= 152) { nshape.charset = Charset.get(s.readInt()); LOGGER.debug(" Charset"); } // Label (text). This label can be NULL in some files, so we // need to explicitly check whether it's a string prior to // parsing it. It can also be present 0 or 2 bytes after the // ShapeAttrs block, so check for both eventualities. { long tmp = s.getFilePointer(); for (int i=0; i<2; ++i) { if (s.readShort() == 8) { s.seek(tmp); nshape.text = parseROIString(s); break; } } if (nshape.text != null) LOGGER.debug(" Text={}", nshape.text); else LOGGER.debug(" Text=NULL"); } // Tag ID if (s.getFilePointer() + 8 > s.length()) break; s.skipBytes(4); LOGGER.debug(" Tag@{}", s.getFilePointer()); nshape.tagID = new Tag(s.readInt(), BaseZeissReader.Context.MAIN); LOGGER.debug(" TagID={}", nshape.tagID); // Font name nshape.fontName = parseROIString(s); if (nshape.fontName == null) break; LOGGER.debug(" Font name={}", nshape.fontName); // Label (name). nshape.name = parseROIString(s); if (nshape.name == null) break; LOGGER.debug(" Name={}", nshape.name); // Handle size and point count. if (s.getFilePointer() + 20 > s.length()) break; s.skipBytes(4); nshape.handleSize = s.readInt(); s.skipBytes(2); nshape.pointCount = s.readInt(); s.skipBytes(6); LOGGER.debug(" Handle size={}", nshape.handleSize); LOGGER.debug(" Point count={}", nshape.pointCount); if (s.getFilePointer() + (8*2*nshape.pointCount) > s.length()) break; nshape.points = new double[nshape.pointCount*2]; for (int p=0; p<nshape.pointCount; p++) { nshape.points[(p*2)] = s.readDouble(); nshape.points[(p*2)+1] = s.readDouble(); } nlayer.shapes.add(nshape); ++roiFound; } if (roiCount != roiFound) { LOGGER.warn("Found {} ROIs, but {} ROIs expected", roiFound, roiCount); } s.close(); } protected String parseROIString(RandomAccessInputStream s) throws IOException { // String is 0x0008 followed by int length for string+NUL. while (s.getFilePointer() < s.length() - 4 && s.readShort() != 8); if (s.getFilePointer() >= s.length() - 8) return null; int strlen = s.readInt(); if (strlen + s.getFilePointer() > s.length()) return null; // Strip off NUL. String text = null; if (strlen >= 2) { // Don't read NUL LOGGER.debug(" String@{} length={}", s.getFilePointer(), strlen); text = s.readString(strlen-2); s.skipBytes(2); } else { s.skipBytes(strlen); } return text; } }