package ij.io; import ij.gui.*; import ij.ImagePlus; import ij.process.*; import java.io.*; import java.util.*; import java.net.*; import java.awt.*; /* ImageJ/NIH Image 64 byte ROI outline header 2 byte numbers are big-endian signed shorts 0-3 "Iout" 4-5 version (>=217) 6-7 roi type 8-9 top 10-11 left 12-13 bottom 14-15 right 16-17 NCoordinates 18-33 x1,y1,x2,y2 (straight line) 34-35 stroke width (v1.43i or later) 36-39 ShapeRoi size (type must be 1 if this value>0) 40-43 stroke color (v1.43i or later) 44-47 fill color (v1.43i or later) 48-49 subtype (v1.43k or later) 50-51 options (v1.43k or later) 52-52 arrow style or aspect ratio (v1.43p or later) 53-53 arrow head size (v1.43p or later) 54-55 rounded rect arc size (v1.43p or later) 56-59 position 60-63 header2 offset 64- x-coordinates (short), followed by y-coordinates */ /** Decodes an ImageJ, NIH Image or Scion Image ROI file. */ public class RoiDecoder { // offsets public static final int VERSION_OFFSET = 4; public static final int TYPE = 6; public static final int TOP = 8; public static final int LEFT = 10; public static final int BOTTOM = 12; public static final int RIGHT = 14; public static final int N_COORDINATES = 16; public static final int X1 = 18; public static final int Y1 = 22; public static final int X2 = 26; public static final int Y2 = 30; public static final int STROKE_WIDTH = 34; public static final int SHAPE_ROI_SIZE = 36; public static final int STROKE_COLOR = 40; public static final int FILL_COLOR = 44; public static final int SUBTYPE = 48; public static final int OPTIONS = 50; public static final int ARROW_STYLE = 52; public static final int ELLIPSE_ASPECT_RATIO = 52; public static final int ARROW_HEAD_SIZE = 53; public static final int ROUNDED_RECT_ARC_SIZE = 54; public static final int POSITION = 56; public static final int HEADER2_OFFSET = 60; public static final int COORDINATES = 64; // header2 offsets public static final int C_POSITION = 4; public static final int Z_POSITION = 8; public static final int T_POSITION = 12; public static final int NAME_OFFSET = 16; public static final int NAME_LENGTH = 20; public static final int OVERLAY_LABEL_COLOR = 24; public static final int OVERLAY_FONT_SIZE = 28; //short public static final int AVAILABLE_BYTE1 = 30; //byte public static final int IMAGE_OPACITY = 31; //byte public static final int IMAGE_SIZE = 32; //int // subtypes public static final int TEXT = 1; public static final int ARROW = 2; public static final int ELLIPSE = 3; public static final int IMAGE = 4; // options public static final int SPLINE_FIT = 1; public static final int DOUBLE_HEADED = 2; public static final int OUTLINE = 4; public static final int OVERLAY_LABELS = 8; public static final int OVERLAY_NAMES = 16; public static final int OVERLAY_BACKGROUNDS = 32; public static final int OVERLAY_BOLD = 64; // types private final int polygon=0, rect=1, oval=2, line=3, freeline=4, polyline=5, noRoi=6, freehand=7, traced=8, angle=9, point=10; private byte[] data; private String path; private InputStream is; private String name; private int size; /** Constructs an RoiDecoder using a file path. */ public RoiDecoder(String path) { this.path = path; } /** Constructs an RoiDecoder using a byte array. */ public RoiDecoder(byte[] bytes, String name) { is = new ByteArrayInputStream(bytes); this.name = name; this.size = bytes.length; } /** Returns the ROI. */ public Roi getRoi() throws IOException { if (path!=null) { File f = new File(path); size = (int)f.length(); if (!path.endsWith(".roi") && size>5242880) throw new IOException("This is not an ROI or file size>5MB)"); name = f.getName(); is = new FileInputStream(path); } data = new byte[size]; int total = 0; while (total<size) total += is.read(data, total, size-total); is.close(); if (getByte(0)!=73 || getByte(1)!=111) //"Iout" throw new IOException("This is not an ImageJ ROI"); int version = getShort(VERSION_OFFSET); int type = getByte(TYPE); int subtype = getShort(SUBTYPE); int top= getShort(TOP); int left = getShort(LEFT); int bottom = getShort(BOTTOM); int right = getShort(RIGHT); int width = right-left; int height = bottom-top; int n = getShort(N_COORDINATES); int options = getShort(OPTIONS); int position = getInt(POSITION); int hdr2Offset = getInt(HEADER2_OFFSET); int channel=0, slice=0, frame=0; int overlayLabelColor=0; int overlayFontSize=0; int imageOpacity=0; int imageSize=0; if (hdr2Offset>0 && hdr2Offset+IMAGE_SIZE+4<=size) { channel = getInt(hdr2Offset+C_POSITION); slice = getInt(hdr2Offset+Z_POSITION); frame = getInt(hdr2Offset+T_POSITION); overlayLabelColor = getInt(hdr2Offset+OVERLAY_LABEL_COLOR); overlayFontSize = getShort(hdr2Offset+OVERLAY_FONT_SIZE); imageOpacity = getByte(hdr2Offset+IMAGE_OPACITY); imageSize = getInt(hdr2Offset+IMAGE_SIZE); } if (name!=null && name.endsWith(".roi")) name = name.substring(0, name.length()-4); boolean isComposite = getInt(SHAPE_ROI_SIZE)>0; Roi roi = null; if (isComposite) { roi = getShapeRoi(); if (version>=218) getStrokeWidthAndColor(roi); roi.setPosition(position); if (channel>0 || slice>0 || frame>0) roi.setPosition(channel, slice, frame); decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize); return roi; } switch (type) { case rect: roi = new Roi(left, top, width, height); int arcSize = getShort(ROUNDED_RECT_ARC_SIZE); if (arcSize>0) roi.setCornerDiameter(arcSize); break; case oval: roi = new OvalRoi(left, top, width, height); break; case line: int x1 = (int)getFloat(X1); int y1 = (int)getFloat(Y1); int x2 = (int)getFloat(X2); int y2 = (int)getFloat(Y2); if (subtype==ARROW) { roi = new Arrow(x1, y1, x2, y2); ((Arrow)roi).setDoubleHeaded((options&DOUBLE_HEADED)!=0); ((Arrow)roi).setOutline((options&OUTLINE)!=0); int style = getByte(ARROW_STYLE); if (style>=Arrow.FILLED && style<=Arrow.HEADLESS) ((Arrow)roi).setStyle(style); int headSize = getByte(ARROW_HEAD_SIZE); if (headSize>=0 && style<=30) ((Arrow)roi).setHeadSize(headSize); } else roi = new Line(x1, y1, x2, y2); //IJ.write("line roi: "+x1+" "+y1+" "+x2+" "+y2); break; case polygon: case freehand: case traced: case polyline: case freeline: case angle: case point: //IJ.log("type: "+type); //IJ.log("n: "+n); //IJ.log("rect: "+left+","+top+" "+width+" "+height); if (n==0) break; int[] x = new int[n]; int[] y = new int[n]; int base1 = COORDINATES; int base2 = base1+2*n; int xtmp, ytmp; for (int i=0; i<n; i++) { xtmp = getShort(base1+i*2); if (xtmp<0) xtmp = 0; ytmp = getShort(base2+i*2); if (ytmp<0) ytmp = 0; x[i] = left+xtmp; y[i] = top+ytmp; //IJ.write(i+" "+getShort(base1+i*2)+" "+getShort(base2+i*2)); } if (type==point) { roi = new PointRoi(x, y, n); break; } int roiType; if (type==polygon) roiType = Roi.POLYGON; else if (type==freehand) { roiType = Roi.FREEROI; if (subtype==ELLIPSE) { double ex1 = getFloat(X1); double ey1 = getFloat(Y1); double ex2 = getFloat(X2); double ey2 = getFloat(Y2); double aspectRatio = getFloat(ELLIPSE_ASPECT_RATIO); roi = new EllipseRoi(ex1,ey1,ex2,ey2,aspectRatio); break; } } else if (type==traced) roiType = Roi.TRACED_ROI; else if (type==polyline) roiType = Roi.POLYLINE; else if (type==freeline) roiType = Roi.FREELINE; else if (type==angle) roiType = Roi.ANGLE; else roiType = Roi.FREEROI; roi = new PolygonRoi(x, y, n, roiType); break; default: throw new IOException("Unrecognized ROI type: "+type); } roi.setName(getRoiName()); // read stroke width, stroke color and fill color (1.43i or later) if (version>=218) { getStrokeWidthAndColor(roi); boolean splineFit = (options&SPLINE_FIT)!=0; if (splineFit && roi instanceof PolygonRoi) ((PolygonRoi)roi).fitSpline(); } if (version>=218 && subtype==TEXT) roi = getTextRoi(roi); if (version>=221 && subtype==IMAGE) roi = getImageRoi(roi, imageOpacity, imageSize); roi.setPosition(position); if (channel>0 || slice>0 || frame>0) roi.setPosition(channel, slice, frame); decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize); return roi; } void decodeOverlayOptions(Roi roi, int version, int options, int color, int fontSize) { Overlay proto = new Overlay(); proto.drawLabels((options&OVERLAY_LABELS)!=0); proto.drawNames((options&OVERLAY_NAMES)!=0); proto.drawBackgrounds((options&OVERLAY_BACKGROUNDS)!=0); if (version>=220) proto.setLabelColor(new Color(color)); boolean bold = (options&OVERLAY_BOLD)!=0; if (fontSize>0 || bold) { proto.setLabelFont(new Font("SansSerif", bold?Font.BOLD:Font.PLAIN, fontSize)); } roi.setPrototypeOverlay(proto); } void getStrokeWidthAndColor(Roi roi) { int strokeWidth = getShort(STROKE_WIDTH); if (strokeWidth>0) roi.setStrokeWidth(strokeWidth); int strokeColor = getInt(STROKE_COLOR); if (strokeColor!=0) { int alpha = (strokeColor>>24)&0xff; roi.setStrokeColor(new Color(strokeColor, alpha!=255)); } int fillColor = getInt(FILL_COLOR); if (fillColor!=0) { int alpha = (fillColor>>24)&0xff; roi.setFillColor(new Color(fillColor, alpha!=255)); } } public Roi getShapeRoi() throws IOException { int type = getByte(TYPE); if (type!=rect) throw new IllegalArgumentException("Invalid composite ROI type"); int top= getShort(TOP); int left = getShort(LEFT); int bottom = getShort(BOTTOM); int right = getShort(RIGHT); int width = right-left; int height = bottom-top; int n = getInt(SHAPE_ROI_SIZE); ShapeRoi roi = null; float[] shapeArray = new float[n]; int base = COORDINATES; for(int i=0; i<n; i++) { shapeArray[i] = getFloat(base); base += 4; } roi = new ShapeRoi(shapeArray); roi.setName(getRoiName()); return roi; } Roi getTextRoi(Roi roi) { Rectangle r = roi.getBounds(); int hdrSize = RoiEncoder.HEADER_SIZE; int size = getInt(hdrSize); int style = getInt(hdrSize+4); int nameLength = getInt(hdrSize+8); int textLength = getInt(hdrSize+12); char[] name = new char[nameLength]; char[] text = new char[textLength]; for (int i=0; i<nameLength; i++) name[i] = (char)getShort(hdrSize+16+i*2); for (int i=0; i<textLength; i++) text[i] = (char)getShort(hdrSize+16+nameLength*2+i*2); Font font = new Font(new String(name), style, size); Roi roi2 = new TextRoi(r.x, r.y, new String(text), font); roi2.setStrokeColor(roi.getStrokeColor()); roi2.setFillColor(roi.getFillColor()); roi2.setName(getRoiName()); return roi2; } Roi getImageRoi(Roi roi, int opacity, int size) { if (size<=0) return roi; Rectangle r = roi.getBounds(); byte[] bytes = new byte[size]; for (int i=0; i<size; i++) bytes[i] = (byte)getByte(COORDINATES+i); ImagePlus imp = new Opener().deserialize(bytes); ImageRoi roi2 = new ImageRoi(r.x, r.y, imp.getProcessor()); roi2.setOpacity(opacity/255.0); return roi2; } String getRoiName() { String fileName = name; int hdr2Offset = getInt(HEADER2_OFFSET); if (hdr2Offset==0) return fileName; int offset = getInt(hdr2Offset+NAME_OFFSET); int length = getInt(hdr2Offset+NAME_LENGTH); if (offset==0 || length==0) return fileName; if (offset+length*2>size) return fileName; char[] name = new char[length]; for (int i=0; i<length; i++) name[i] = (char)getShort(offset+i*2); return new String(name); } int getByte(int base) { return data[base]&255; } int getShort(int base) { int b0 = data[base]&255; int b1 = data[base+1]&255; int n = (short)((b0<<8) + b1); if (n<-5000) n = (b0<<8) + b1; // assume n>32767 and unsigned return n; } int getInt(int base) { int b0 = data[base]&255; int b1 = data[base+1]&255; int b2 = data[base+2]&255; int b3 = data[base+3]&255; return ((b0<<24) + (b1<<16) + (b2<<8) + b3); } float getFloat(int base) { return Float.intBitsToFloat(getInt(base)); } /** Opens an ROI from a byte array. */ public static Roi openFromByteArray(byte[] bytes) { Roi roi = null; try { RoiDecoder decoder = new RoiDecoder(bytes, null); roi = decoder.getRoi(); } catch (IOException e) { return null; } return roi; } }