diff common/src/main/java/digilib/image/ImageLoaderDocuImage.java @ 903:7779b37d1d05

refactored into maven modules per servlet type. can build servlet-api 2.3 and 3.0 via profile now!
author robcast
date Tue, 26 Apr 2011 20:24:31 +0200
parents servlet/src/main/java/digilib/image/ImageLoaderDocuImage.java@ba1eb2d821a2
children 28d007673346
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/src/main/java/digilib/image/ImageLoaderDocuImage.java	Tue Apr 26 20:24:31 2011 +0200
@@ -0,0 +1,650 @@
+/* ImageLoaderDocuImage -- Image class implementation using JDK 1.4 ImageLoader
+
+ Digital Image Library servlet components
+
+ Copyright (C) 2002 - 2011 Robert Casties (robcast@mail.berlios.de)
+
+ 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.
+ 
+ Please read license.txt for the full details. A copy of the GPL
+ may be found at http://www.gnu.org/copyleft/lgpl.html
+
+ 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 digilib.image;
+
+import java.awt.Image;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.color.ColorSpace;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BandCombineOp;
+import java.awt.image.BufferedImage;
+import java.awt.image.ByteLookupTable;
+import java.awt.image.ColorConvertOp;
+import java.awt.image.ColorModel;
+import java.awt.image.ConvolveOp;
+import java.awt.image.IndexColorModel;
+import java.awt.image.Kernel;
+import java.awt.image.LookupOp;
+import java.awt.image.LookupTable;
+import java.awt.image.RescaleOp;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+import java.util.Iterator;
+
+import javax.imageio.IIOImage;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.ImageWriter;
+import javax.imageio.stream.FileImageInputStream;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import javax.servlet.ServletException;
+
+import digilib.io.FileOpException;
+import digilib.io.FileOps;
+import digilib.io.ImageInput;
+import digilib.util.ImageSize;
+
+/** Implementation of DocuImage using the ImageLoader API of Java 1.4 and Java2D. */
+public class ImageLoaderDocuImage extends ImageInfoDocuImage {
+    
+	/** image object */
+	protected BufferedImage img;
+	
+	/** interpolation type */
+	protected RenderingHints renderHint = null;
+
+	/** convolution kernels for blur() */
+	protected static Kernel[] convolutionKernels = {
+	        null,
+	        new Kernel(1, 1, new float[] {1f}),
+            new Kernel(2, 2, new float[] {0.25f, 0.25f, 0.25f, 0.25f}),
+            new Kernel(3, 3, new float[] {1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f})
+	};
+
+	/* lookup tables for inverting images (byte) */
+	protected static LookupTable invertSingleByteTable;
+    protected static LookupTable invertRgbaByteTable;
+    protected static boolean needsInvertRgba = false;
+	/* RescaleOp for contrast/brightness operation */
+    protected static boolean needsRescaleRgba = false;
+    /* lookup table for false-color */
+    protected static LookupTable mapBgrByteTable;
+    protected static boolean needsMapBgr = false;
+    
+	static {
+	    /*
+	     * create static lookup tables
+	     */
+		byte[] invertByte = new byte[256];
+		byte[] orderedByte = new byte[256];
+		byte[] nullByte = new byte[256];
+        byte[] mapR = new byte[256];
+        byte[] mapG = new byte[256];
+        byte[] mapB = new byte[256];
+		for (int i = 0; i < 256; ++i) {
+		    // counting down
+			invertByte[i] = (byte) (256 - i);
+			// counting up
+			orderedByte[i] = (byte) i;
+			// constant 0
+			nullByte[i] = 0;
+			// three overlapping slopes
+			if (i < 64) {
+			    mapR[i] = 0;
+			    mapG[i] = (byte) (4 * i);
+			    mapB[i] = (byte) 255;
+			} else if (i >= 64 && i < 192) {
+                mapR[i] = (byte) (2 * (i - 64));
+                mapG[i] = (byte) 255;
+                mapB[i] = (byte) (255 - 2 * (i - 64));
+			} else {
+                mapR[i] = (byte) 255;
+                mapG[i] = (byte) (255 - (4 * (i - 192)));
+                mapB[i] = 0;
+			}
+		}
+		// should(!) work for all color models
+		invertSingleByteTable = new ByteLookupTable(0, invertByte);
+		// but doesn't work with alpha channel on all platforms
+		String ver = System.getProperty("java.version");
+		String os =  System.getProperty("os.name");
+		logger.debug("os="+os+" ver="+ver);
+		if (os.startsWith("Linux") && ver.startsWith("1.6")) {
+			// GRAB(WTF?) works in Linux JDK1.6 with transparency
+			invertRgbaByteTable = new ByteLookupTable(0, new byte[][] {
+					invertByte, invertByte, orderedByte, invertByte});
+			needsInvertRgba = true;
+			needsRescaleRgba = true;
+			needsMapBgr = true;
+		} else {
+			invertRgbaByteTable = invertSingleByteTable;
+		}
+		// this hopefully works for all
+        mapBgrByteTable = new ByteLookupTable(0, new byte[][] {
+                mapR, mapG, mapB});
+	}
+	
+	/** the size of the current image */
+    protected ImageSize imageSize;
+	
+	
+	/* loadSubimage is supported. */
+	public boolean isSubimageSupported() {
+		return true;
+	}
+
+	public void setQuality(int qual) {
+		quality = qual;
+		renderHint = new RenderingHints(null);
+		// hint.put(RenderingHints.KEY_ANTIALIASING,
+		// RenderingHints.VALUE_ANTIALIAS_OFF);
+		// setup interpolation quality
+		if (qual > 0) {
+			logger.debug("quality q1");
+			renderHint.put(RenderingHints.KEY_INTERPOLATION,
+					RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+		} else {
+			logger.debug("quality q0");
+			renderHint.put(RenderingHints.KEY_INTERPOLATION,
+					RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
+		}
+	}
+
+    /* returns the size of the current image */
+    public ImageSize getSize() {
+        if (imageSize == null) {
+            int h = 0;
+            int w = 0;
+            try {
+                if (img == null) {
+                    ImageReader reader = getReader(input);
+                    // get size from ImageReader
+                    h = reader.getHeight(0);
+                    w = reader.getWidth(0);
+                } else {
+                    // get size from image
+                    h = img.getHeight();
+                    w = img.getWidth();
+                }
+                imageSize = new ImageSize(w, h);
+            } catch (IOException e) {
+                logger.debug("error in getSize:", e);
+            }
+        }
+        return imageSize;
+    }
+
+	/* returns a list of supported image formats */
+	public Iterator<String> getSupportedFormats() {
+		String[] formats = ImageIO.getReaderFormatNames();
+		return Arrays.asList(formats).iterator();
+	}
+
+    /* Check image size and type and store in ImageInput */
+    public ImageInput identify(ImageInput input) throws IOException {
+        // try parent method first
+        ImageInput ii = super.identify(input);
+        if (ii != null) {
+            return ii;
+        }
+        logger.debug("identifying (ImageIO) " + input);
+        ImageReader reader = null;
+        try {
+            /*
+             * try ImageReader
+             */
+            reader = getReader(input);
+            // set size
+            ImageSize d = new ImageSize(reader.getWidth(0), reader.getHeight(0));
+            input.setSize(d);
+            // set mime type
+            if (input.getMimetype() == null) {
+                if (input.hasFile()) {
+                    String t = FileOps.mimeForFile(input.getFile());
+                    input.setMimetype(t);
+                } else {
+                    // FIXME: is format name a mime type???
+                    String t = reader.getFormatName();
+                    input.setMimetype(t);
+                }
+            }
+            return input;
+        } catch (FileOpException e) {
+            // maybe just our class doesn't know what to do
+            logger.error("ImageLoaderDocuimage unable to identify:", e);
+            return null;
+        } finally {
+            if (reader != null) {
+                reader.dispose();
+            }
+        }
+    }
+    
+    /* load image file */
+	public void loadImage(ImageInput ii) throws FileOpException {
+		logger.debug("loadImage: " + ii);
+		this.input = ii;
+		try {
+		    if (ii.hasImageInputStream()) {
+                img = ImageIO.read(ii.getImageInputStream());
+		    } else if (ii.hasFile()) {
+		        img = ImageIO.read(ii.getFile());
+		    }
+		} catch (IOException e) {
+			throw new FileOpException("Error reading image.");
+		}
+	}
+
+	/**
+	 * Get an ImageReader for the image file.
+	 * 
+	 * @return
+	 */
+	public ImageReader getReader(ImageInput input) throws IOException {
+		logger.debug("get ImageReader for " + input);
+		ImageInputStream istream = null;
+		if (input.hasImageInputStream()) {
+			// stream input
+			istream = input.getImageInputStream();
+		} else if (input.hasFile()) {
+			// file only input
+			RandomAccessFile rf = new RandomAccessFile(input.getFile(), "r");
+			istream = new FileImageInputStream(rf);
+		} else {
+			throw new FileOpException("Unable to get data from ImageInput");
+		}
+		Iterator<ImageReader> readers;
+		String mt = null;
+		if (input.hasMimetype()) {
+	        // check hasMimetype first or we might get into a loop
+		    mt = input.getMimetype();
+		} else {
+		    // try file extension
+            mt = FileOps.mimeForFile(input.getFile());
+		}
+		if (mt == null) {
+			logger.debug("No mime-type. Trying automagic.");
+			readers = ImageIO.getImageReaders(istream);
+		} else {
+			logger.debug("File type:" + mt);
+			readers = ImageIO.getImageReadersByMIMEType(mt);
+		}
+		if (!readers.hasNext()) {
+			throw new FileOpException("Can't find Reader to load File!");
+		}
+		ImageReader reader = readers.next();
+		/* are there more readers? */
+		logger.debug("ImageIO: this reader: " + reader.getClass());
+		/* while (readers.hasNext()) {
+			logger.debug("ImageIO: next reader: " + readers.next().getClass());
+		} */
+		reader.setInput(istream);
+		return reader;
+	}
+
+	/* Load an image file into the Object. */
+	public void loadSubimage(ImageInput ii, Rectangle region, int prescale)
+			throws FileOpException {
+		logger.debug("loadSubimage");
+        this.input = ii;
+        ImageReader reader = null;
+		try {
+			reader = getReader(ii);
+			// set up reader parameters
+			ImageReadParam readParam = reader.getDefaultReadParam();
+			readParam.setSourceRegion(region);
+			if (prescale > 1) {
+				readParam.setSourceSubsampling(prescale, prescale, 0, 0);
+			}
+			// read image
+			logger.debug("loading..");
+			img = reader.read(0, readParam);
+			logger.debug("loaded");
+			/* downconversion of highcolor images seems not to work
+	        if (img.getColorModel().getComponentSize(0) > 8) {
+	            logger.debug("converting to 8bit");
+	            BufferedImage dest = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
+	            dest.createGraphics().drawImage(img, null, 0, 0);
+	            img = dest;
+	        } */
+		} catch (IOException e) {
+			throw new FileOpException("Unable to load File!");
+		} finally {
+		    if (reader != null) {
+		        reader.dispose();
+		    }
+		}
+	}
+
+	/* write image of type mt to Stream */
+	public void writeImage(String mt, OutputStream ostream)
+			throws ImageOpException, ServletException {
+		logger.debug("writeImage");
+		// setup output
+		ImageWriter writer = null;
+		ImageOutputStream imgout = null;
+		try {
+			imgout = ImageIO.createImageOutputStream(ostream);
+			if (mt == "image/jpeg") {
+				/*
+				 * JPEG doesn't do transparency so we have to convert any RGBA
+				 * image to RGB or we the client will think its CMYK :-( *Java2D BUG*
+				 */
+				if (img.getColorModel().hasAlpha()) {
+					logger.debug("BARF: JPEG with transparency!!");
+                    BufferedImage rgbImg = new BufferedImage(img.getWidth(),
+                            img.getHeight(), BufferedImage.TYPE_INT_RGB);
+					rgbImg.createGraphics().drawImage(img, null, 0, 0);
+					img = rgbImg;
+				}
+				writer = ImageIO.getImageWritersByFormatName("jpeg").next();
+				if (writer == null) {
+					throw new ImageOpException("Unable to get JPEG writer");
+				}
+				ImageWriteParam param = writer.getDefaultWriteParam();
+				if (quality > 1) {
+					// change JPEG compression quality
+					param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+					param.setCompressionQuality(0.9f);
+				}
+				writer.setOutput(imgout);
+				// render output
+				logger.debug("writing JPEG");
+				writer.write(null, new IIOImage(img, null, null), param);
+			} else if (mt == "image/png") {
+				// render output
+				writer = ImageIO.getImageWritersByFormatName("png").next();
+				if (writer == null) {
+					throw new ImageOpException("Unable to get PNG writer");
+				}
+				writer.setOutput(imgout);
+				logger.debug("writing PNG");
+				writer.write(img);
+			} else {
+				// unknown mime type
+				throw new ImageOpException("Unknown mime type: " + mt);
+			}
+
+		} catch (IOException e) {
+		    logger.error("Error writing image:", e);
+			throw new ServletException("Error writing image:", e);
+		}
+		// TODO: should we: finally { writer.dispose(); }
+	}
+
+    public void scale(double scaleX, double scaleY) throws ImageOpException {
+        logger.debug("scale: " + scaleX);
+        /* for downscaling in high quality the image is blurred first */
+        if ((scaleX <= 0.5) && (quality > 1)) {
+            int bl = (int) Math.floor(1 / scaleX);
+            blur(bl);
+        }
+        /* then scaled */
+        AffineTransformOp scaleOp = new AffineTransformOp(
+                AffineTransform.getScaleInstance(scaleX, scaleY), renderHint);
+        img = scaleOp.filter(img, null);
+        logger.debug("scaled to " + img.getWidth() + "x" + img.getHeight()
+                + " img=" + img);
+    }
+
+	public void blur(int radius) throws ImageOpException {
+		logger.debug("blur: " + radius);
+		// minimum radius is 2
+		int klen = Math.max(radius, 2);
+		Kernel blur = null;
+		if (klen < convolutionKernels.length) {
+		    // use precalculated Kernel
+            blur = convolutionKernels[klen];
+		} else {
+            // calculate our own kernel
+            int ksize = klen * klen;
+            // kernel is constant 1/k
+            float f = 1f / ksize;
+            float[] kern = new float[ksize];
+            for (int i = 0; i < ksize; ++i) {
+                kern[i] = f;
+            }
+            blur = new Kernel(klen, klen, kern);
+		}
+		// blur with convolve operation
+		ConvolveOp blurOp = new ConvolveOp(blur, ConvolveOp.EDGE_NO_OP,
+				renderHint);
+		BufferedImage dest = null;
+        // blur needs explicit destination image type for 3BYTE_BGR *Java2D BUG*
+		if (img.getType() == BufferedImage.TYPE_3BYTE_BGR) {
+		    logger.debug("blur: fixing destination image type");
+		    dest = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
+		}
+		img = blurOp.filter(img, dest);
+		logger.debug("blurred: "+img);
+	}
+
+	public void crop(int x_off, int y_off, int width, int height)
+			throws ImageOpException {
+		// setup Crop
+		img = img.getSubimage(x_off, y_off, width, height);
+		logger.debug("CROP:" + img.getWidth() + "x"
+				+ img.getHeight());
+	}
+
+	public void rotate(double angle) throws ImageOpException {
+        logger.debug("rotate: " + angle);
+		// setup rotation
+		double rangle = Math.toRadians(angle);
+		// center of rotation is center of image
+        double w = img.getWidth();
+        double h = img.getHeight();
+		double x = (w / 2);
+		double y = (h / 2);
+        AffineTransform trafo = AffineTransform.getRotateInstance(rangle, x, y);
+		AffineTransformOp rotOp = new AffineTransformOp(trafo, renderHint);
+        // rotate bounds to see how much of the image would be off screen
+		Rectangle2D rotbounds = rotOp.getBounds2D(img);
+		double xoff = rotbounds.getX();
+		double yoff = rotbounds.getY();
+		if (Math.abs(xoff) > epsilon || Math.abs(yoff) > epsilon) {
+		    // move image back on screen
+		    logger.debug("move rotation: xoff="+xoff+" yoff="+yoff);
+		    trafo.preConcatenate(AffineTransform.getTranslateInstance(-xoff, -yoff));
+	        rotOp = new AffineTransformOp(trafo, renderHint);
+		}
+		// transform image
+		img = rotOp.filter(img, null);
+		logger.debug("rotated: "+img);
+	}
+
+	public void mirror(double angle) throws ImageOpException {
+        logger.debug("mirror: " + angle);
+		// setup mirror
+		double mx = 1;
+		double my = 1;
+		double tx = 0;
+		double ty = 0;
+		if (Math.abs(angle - 0) < epsilon) { // 0 degree
+			mx = -1;
+			tx = img.getWidth();
+		} else if (Math.abs(angle - 90) < epsilon) { // 90 degree
+			my = -1;
+			ty = img.getHeight();
+		} else if (Math.abs(angle - 180) < epsilon) { // 180 degree
+			mx = -1;
+			tx = img.getWidth();
+		} else if (Math.abs(angle - 270) < epsilon) { // 270 degree
+			my = -1;
+			ty = img.getHeight();
+		} else if (Math.abs(angle - 360) < epsilon) { // 360 degree
+			mx = -1;
+			tx = img.getWidth();
+		} else {
+		    logger.error("invalid mirror angle "+angle);
+		    return;
+		}
+		AffineTransformOp mirOp = new AffineTransformOp(new AffineTransform(mx,
+				0, 0, my, tx, ty), renderHint);
+		img = mirOp.filter(img, null);
+	}
+
+    public void enhance(float mult, float add) throws ImageOpException {
+        RescaleOp op = null;
+        logger.debug("enhance: img=" + img);
+        if (needsRescaleRgba) {
+            /*
+             * Only one constant should work regardless of the number of bands
+             * according to the JDK spec. Doesn't work on JDK 1.4 for OSX and
+             * Linux (at least).
+             * 
+             * The number of constants must match the number of bands in the
+             * image.
+             */
+            int ncol = img.getColorModel().getNumComponents();
+            float[] dm = new float[ncol];
+            float[] da = new float[ncol];
+            for (int i = 0; i < ncol; i++) {
+                dm[i] = (float) mult;
+                da[i] = (float) add;
+            }
+            op = new RescaleOp(dm, da, null);
+        } else {
+            op = new RescaleOp(mult, add, renderHint);
+        }
+        op.filter(img, img);
+    }
+
+    public void enhanceRGB(float[] rgbm, float[] rgba) throws ImageOpException {
+        logger.debug("enhanceRGB: rgbm="+rgbm+" rgba="+rgba);
+        /*
+         * The number of constants must match the number of bands in the image.
+         * We do only 3 (RGB) bands.
+         */
+        int ncol = img.getColorModel().getNumColorComponents();
+        if ((ncol != 3) || (rgbm.length != 3) || (rgba.length != 3)) {
+            logger.error("enhanceRGB: unknown number of color bands or coefficients ("
+                            + ncol + ")");
+            return;
+        }
+        if (img.getColorModel().hasAlpha()) {
+            // add constant for alpha
+            rgbm = new float[] {rgbm[0], rgbm[1], rgbm[2], 1};
+            rgba = new float[] {rgba[0], rgba[1], rgba[2], 0};
+        }
+        RescaleOp scaleOp = new RescaleOp(rgbm, rgba, renderHint);
+        scaleOp.filter(img, img);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * digilib.image.DocuImageImpl#colorOp(digilib.image.DocuImage.ColorOps)
+     */
+    public void colorOp(ColorOp colop) throws ImageOpException {
+        if (colop == ColorOp.GRAYSCALE) {
+            /*
+             * convert image to grayscale
+             */
+            logger.debug("Color op: grayscaling");
+            ColorModel cm = img.getColorModel();
+            if (cm.getNumColorComponents() < 3) {
+                // grayscale already
+                logger.debug("Color op: not grayscaling");
+                return;
+            }
+            ColorConvertOp op = new ColorConvertOp(
+                    ColorSpace.getInstance(ColorSpace.CS_GRAY), renderHint);
+            // let filter create new image
+            img = op.filter(img, null);
+        } else if (colop == ColorOp.NTSC_GRAY) {
+            /*
+             * convert image to grayscale NTSC-style: luminance = 0.2989*red +
+             * 0.5870*green + 0.1140*blue
+             */
+            logger.debug("Color op: NTSC gray");
+            logger.debug("img="+img);
+            ColorModel cm = img.getColorModel();
+            if (cm.getNumColorComponents() < 3 || cm instanceof IndexColorModel) {
+                // grayscale already or not possible
+                logger.debug("Color op: unable to NTSC gray");
+                return;
+            }
+            float[][] combineFn = new float[1][4];
+            combineFn[0] = new float[] { 0.299f, 0.587f, 0.114f, 0f };
+            BandCombineOp op = new BandCombineOp(combineFn, renderHint);
+            // BandCombineOp only works on Rasters so we create a
+            // new image and use its Raster
+            BufferedImage dest = new BufferedImage(img.getWidth(),
+                    img.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
+            op.filter(img.getRaster(), dest.getRaster());
+            img = dest;
+        } else if (colop == ColorOp.INVERT) {
+            /*
+             * invert colors i.e. invert every channel
+             */
+            logger.debug("Color op: inverting");
+            LookupTable invtbl = null;
+            ColorModel cm = img.getColorModel();
+            if (cm instanceof IndexColorModel) {
+                // invert not possible
+                // TODO: should we convert?
+                logger.debug("Color op: unable to invert");
+                return;
+            }
+            if (needsInvertRgba && cm.hasAlpha()) {
+                // fix for some cases
+                invtbl = invertRgbaByteTable;
+            } else {
+                invtbl = invertSingleByteTable;
+            }
+            LookupOp op = new LookupOp(invtbl, renderHint);
+            logger.debug("colop: image=" + img);
+            op.filter(img, img);
+        } else if (colop == ColorOp.MAP_GRAY_BGR) {
+            /*
+             * false color image from grayscale (0: blue, 128: green, 255: red)
+             */
+            logger.debug("Color op: map_gray_bgr");
+            // convert to grayscale
+            ColorConvertOp grayOp = new ColorConvertOp(
+                    ColorSpace.getInstance(ColorSpace.CS_GRAY), renderHint);
+            // create new 3-channel image
+            int destType = BufferedImage.TYPE_INT_RGB;
+            if (needsMapBgr) {
+                // special case for broken Java2Ds
+                destType = BufferedImage.TYPE_3BYTE_BGR;
+            }
+            BufferedImage dest = new BufferedImage(img.getWidth(),
+                    img.getHeight(), destType);
+            img = grayOp.filter(img, dest);
+            logger.debug("map_gray: image=" + img);
+            // convert to false color
+            LookupOp mapOp = new LookupOp(mapBgrByteTable, renderHint);
+            mapOp.filter(img, img);
+            logger.debug("mapped image=" + img);
+        }
+    }
+
+	public void dispose() {
+	    // is this necessary?
+		img = null;
+	}
+
+	public Image getAwtImage(){
+		return (Image) img;
+	}
+	
+	
+}