/********************************************************************************* * TotalCross Software Development Kit * * Copyright (C) 1998, 1999 Wabasoft <www.wabasoft.com> * * Copyright (C) 2000-2012 SuperWaba Ltda. * * All Rights Reserved * * * * This library and virtual machine 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. * * * * This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 * * A copy of this license is located in file license.txt at the root of this * * SDK or can be downloaded here: * * http://www.gnu.org/licenses/lgpl-3.0.txt * * * *********************************************************************************/ package totalcross.ui.image; import java.awt.GraphicsEnvironment; import java.io.ByteArrayInputStream; import javax.imageio.ImageIO; import totalcross.*; import totalcross.io.*; import totalcross.sys.*; import totalcross.ui.*; import totalcross.ui.gfx.*; import totalcross.util.*; import totalcross.util.zip.*; /** * Image is a rectangular image. * <p> * You can draw into an image and copy an image to a surface using a Graphics * object. Images are always 24bpp, and TotalCross supports PNG and JPEG formats at device, * and PNG/JPEG/GIF/BMP at desktop (the last two are converted to png when deploying). * The deployed png may contain transparency information which is correctly handled. * <p> * This is a code sample of how to run a multi-framed image. Note that it will work * in the same way in desktop or in device. * <pre> Image img = new Image("alligator.gif"); add(new ImageControl(img),LEFT,TOP,FILL,PREFERRED); for (int i = 0; i < 100; i++) { repaintNow(); Vm.sleep(200); img.nextFrame(); } * </pre> * Some transformation methods returns a new instance of this image and other apply to the current instance. * To preserve an image with a single frame, use <code>getFrameInstance(0)</code>. * * Note: TotalCross does not support grayscale PNG with alpha-channel. Convert the image to true-color with * alpha-channel and it will work fine (the only backdraw is that the new image will be bigger). * * The hwScale methods should not be used in images that are shown using transition effects. * * @see Graphics */ public class Image extends GfxSurface { protected int width; protected int height; /** Contains the pixels of this image. */ int[] pixels; /** The number of frames of this image, if derived from a multi-frame gif. */ private int frameCount=1; /** A textual description stored in the PNG image. */ public String comment; private Graphics gfx; private Object pixelsOfAllFrames; private int currentFrame=-1, widthOfAllFrames; /** Dumb field to keep compilation compatibility with TC 1 */ public static final int NO_TRANSPARENT_COLOR = -2; /** Dumb field to keep compilation compatibility with TC 1 */ public int transparentColor = Color.WHITE; /** Dumb field to keep compilation compatibility with TC 1 */ public boolean useAlpha; // guich@tc126_12 /** A global alpha mask to be applied to the whole image when drawing it, ranging from 0 to 255. */ public int alphaMask=255; /** Hardware accellerated scaling. The original image is scaled up or down * by the video card when its displayed. In high end devices, the quality * is the same of the algorithm used in smooth instances. * * Works only if <code>Settings.isOpenGL</code> or on JavaSE. * If you set this in non-opengl environments, nothing will happen; you should use the * hwScaledBy, getHwScaledInstance and hwScaledFixedAspectRatio methods. * * To apply the changes, just call <code>repaint()</code>. * @see #setHwScaleFixedAspectRatio(int,boolean) * @see #hwScaledBy(double, double) * @see #hwScaledFixedAspectRatio(int, boolean) * @see #getHwScaledInstance(int, int) * @since TotalCross 2.0 */ public double hwScaleW=1,hwScaleH=1; /** Sets the hwScaleW and hwScaleH fields based on the given new size. * Does not work on Win32. * @see #hwScaleH * @see #hwScaleW * @since TotalCross 2.0 */ public void setHwScaleFixedAspectRatio(int newSize, boolean isHeight) { int w = !isHeight ? newSize : (newSize * width / height); int h = isHeight ? newSize : (newSize * height / width); hwScaleW = (double)w / width; hwScaleH = (double)h / height; } /** At non OpenGL devices, is the same of smoothScaledFixedAspectRatio; * At openGL ones, this method shares all image informations * while changing only the hwScaleW/hwScaleH parameters. * @since TotalCross 2.0 */ public Image hwScaledFixedAspectRatio(int newSize, boolean isHeight) throws ImageException { return smoothScaledFixedAspectRatio(newSize, isHeight); } /** At non OpenGL devices, is the same of getSmoothScaledInstance; * At openGL ones, this method shares all image informations * while changing only the hwScaleW/hwScaleH parameters. * @since TotalCross 2.0 */ public Image getHwScaledInstance(int width, int height) throws ImageException { return getSmoothScaledInstance(width, height); } /** At non OpenGL devices, is the same of smoothScaledBy; * At openGL ones, this method shares all image informations * while changing only the hwScaleW/hwScaleH parameters. * @since TotalCross 2.0 */ public Image hwScaledBy(double scaleX, double scaleY) throws ImageException { return smoothScaledBy(scaleX, scaleY); } /** * Creates an image of the specified width and height. The image has * a color depth (number of bitplanes) and color map that matches the * default drawing surface. * Here is an example of use: * <pre> * Image img = new Image(100,100); * Graphics g = img.getGraphics(); * g.backColor = Color.WHITE; * g.fillRect(25,25,50,50); * ... * Graphics screenG = getGraphics(); * screenG.drawImage(img,CENTER,CENTER); * </pre> */ public Image(int width, int height) throws ImageException { this.width = width; this.height = height; try { pixels = new int[height*width]; // just create the pixels array } catch (OutOfMemoryError oome) {throw new ImageException("Out of memory: cannot allocate "+width+"x"+height+" offscreen image.");} init(); } /** Used only at desktop to get the image's pixels. */ public int[] getPixels() { return pixels; } /** * Loads and constructs an image from a file. The path given is the path to the * image file. The file must be in 2, 16, 256, 24bpp color compressed (RLE) or uncompressed BMP bitmap * format, or a PNG file, or a GIF file, or a JPEG file. If the image cannot be loaded, an ImageException will be thrown. * @throws totalcross.ui.image.ImageException When the file was not found. */ public Image(String path) throws ImageException, IOException { imageLoad(path); if (width == 0) throw new ImageException("Could not load image, file not found: "+path); init(); } /** Loads a BMP, JPEG, GIF or PNG image from a totalcross.io.Stream. Note that Gif and BMP are supported only at desktop. * Note that all the bytes of the given stream will be fetched, even those bytes that may follow this Image. * @throws totalcross.io.IOException */ public Image(Stream s) throws ImageException, totalcross.io.IOException { ByteArrayStream bas = new ByteArrayStream(8192); byte[] buf = new byte[1024]; while (true) { int n = s.readBytes(buf,0,buf.length); if (n <= 0) break; bas.writeBytes(buf, 0, n); } imageParse(bas.getBuffer(), bas.getPos()); if (width == 0) throw new ImageException("Error on bmp with "+bas.getPos()+" bytes length description"); init(); } /** Sets the transparent color of this image. A new image is NOT created. * * @return The image itself * @since TotalCross 2.0 */ public Image setTransparentColor(int color) { int[] pixels = (int[]) ((frameCount == 1) ? this.pixels : this.pixelsOfAllFrames); // guich@tc100b5_40 for (int i = pixels.length; --i >= 0;) { int p = pixels[i] & 0xFFFFFF; pixels[i] = (p == color) ? color : p | 0xFF000000; // if is the transparent color, set the alpha to 0, otherwise, set to full bright } return this; } /** Parses an image from the given byte array. Note that the byte array must * specify the full JPEG/PNG image, with headers (Gif/Bmp are supported at desktop only). * Here is a code example: <pre> * // create the image and fill it with something * Image img = new Image(160,160); * Graphics g = img.getGraphics(); * for (int i =0; i < 16; i++) * { * g.backColor = Color.getRGB(10*i,10*i,10*i); * g.fillRect(i*10,0,10,160); * } * img.applyChanges(); * // save the bmp in a byte stream * ByteArrayStream bas = new ByteArrayStream(4096); * DataStream ds = new DataStream(bas); * int totalBytesWritten = img.createPng(ds); * // parse the saved png * Image im = new Image(bas.getBuffer()); // Caution! the buffer may be greater than totalBytesWritten, but when parsing theres no problem. * if (im.getWidth() > 0) // successfully parsed? * { * getGraphics().drawImage(im,CENTER,CENTER); * Vm.sleep(2000); * } * </pre> * Caution: if reading a JPEG file, the original array contents will be changed! * @throws totalcross.ui.image.ImageException Thrown when something was wrong with the image. */ public Image(byte []fullDescription) throws ImageException { this(fullDescription, fullDescription.length); } /** Parses an image from the given byte array with the specified length. Note that the byte array must * specify the full JPEG/PNG image, with headers (Gif/Bmp are supported at desktop only). * Here is a code example: <pre> * // create the image and fill it with something * Image img = new Image(160,160); * Graphics g = img.getGraphics(); * for (int i =0; i < 16; i++) * { * g.backColor = Color.getRGB(10*i,10*i,10*i); * g.fillRect(i*10,0,10,160); * } * img.applyChanges(); * // save the bmp in a byte stream * ByteArrayStream bas = new ByteArrayStream(4096); * DataStream ds = new DataStream(bas); * int totalBytesWritten = img.createPng(ds); * // parse the saved png * Image im = new Image(bas.getBuffer()); // Caution! the buffer may be greater than totalBytesWritten, but when parsing theres no problem. * if (im.getWidth() > 0) // successfully parsed? * { * getGraphics().drawImage(im,CENTER,CENTER); * Vm.sleep(2000); * } * </pre> * Caution: if reading a JPEG file, the original array contents will be changed! * @throws totalcross.ui.image.ImageException Thrown when something was wrong with the image. */ public Image(byte []fullDescription, int length) throws ImageException { imageParse(fullDescription, length); if (width == 0) throw new ImageException(fullDescription==null?"Description is null":("Error on image with "+fullDescription.length+" bytes length description")); init(); } private void init() throws IllegalArgumentException, IllegalStateException, ImageException { // frame count information? if (comment != null && comment.startsWith("FC=")) try {setFrameCount(Convert.toInt(comment.substring(3)));} catch (InvalidNumberException ine) {} // init the Graphics gfx = new Graphics(this); gfx.refresh(0,0,width,height,0,0,null); } /** * Sets the frame count for this image. The width may be a multiple of the frame count. After the frame count is * set, it cannot be changed. * * @throws IllegalArgumentException * @throws IllegalStateException * @throws ImageException * @since TotalCross 1.0 */ public void setFrameCount(int n) throws IllegalArgumentException, IllegalStateException, ImageException { if (frameCount > 1 && n != frameCount) throw new IllegalStateException("The frame count can only be set once."); if (n < 1) throw new IllegalArgumentException("Argument 'n' must have a positive value"); if (n != frameCount && n > 1 && frameCount <= 1) try { frameCount = n; comment = "FC="+n; widthOfAllFrames = width; width /= frameCount; // the pixels will hold the pixel of a single frame pixelsOfAllFrames = pixels; pixels = new int[width * height]; setCurrentFrame(0); } catch (OutOfMemoryError oome) {throw new ImageException("Not enough memory to create the single frame");} } /** Returns the frame count of this image. * @since TotalCross 1.0 */ public int getFrameCount() { return frameCount; } /** Move the contents of the given frame to the currently visible pixels. * @since TotalCross 1.0 */ final public void setCurrentFrame(int nr) { if (frameCount <= 1 || nr == currentFrame) return; if (nr < 0) nr = frameCount-1; else if (nr >= frameCount) nr = 0; currentFrame = nr; for (int y = height-1; y >= 0; y--) Vm.arrayCopy(pixelsOfAllFrames, nr * width + y * widthOfAllFrames, pixels, y * width, width); } /** Returns the current frame in a multi-frame image. * @since TotalCross 1.0 */ public int getCurrentFrame() { return currentFrame; } /** Move to next frame in a multi-frame image. * @since TotalCross 1.0 */ public void nextFrame() { if (frameCount > 1) setCurrentFrame(currentFrame+1); } /** Move to the previous frame in a multi-frame image. * @since TotalCross 1.0 */ public void prevFrame() { if (frameCount > 1) setCurrentFrame(currentFrame-1); } /** Returns the height of the image. You can check if the image is ok comparing this with zero. */ public int getHeight() { return (int)(height * hwScaleH); } /** Returns the width of the image. You can check if the image is ok comparing this with zero. */ public int getWidth() { return (int)(width * hwScaleW); } /** Returns a new Graphics instance that can be used to drawing in this image. */ public Graphics getGraphics() { if (Launcher.instance.mainWindow != null) gfx.setFont(MainWindow.getDefaultFont()); // avoid loading the font if running from tc.Deploy gfx.refresh(0,0,width,height,0,0,null); return gfx; } /** Applies any pending changes made in this image. * In Open GL platforms, creates a texture for this image. This is already done, lazily, when the image * is going to be painted. If you want to speedup paint, call this method as soon as any changes in the image * are finished. * * In non-open gl platforms, does nothing. * @since TotalCross 2 */ public void applyChanges() { } /** In OpenGL platforms, apply changes to the current texture and * frees the memory used for the pixels in internal memory (the * image can, however, be drawn on screen because the texture will * be ready). Calling getGraphics after this method will return a * null reference. * * In non-OpenGL, does nothing. * @since TotalCross 2.0 */ public void lockChanges() { } /** Changes all the pixels of the image from one color to the other. * The current value of the transparent color is not changed. * Using this routine, you can change the colors to any other you want. * * Note this replaces a single solid color by another solid color. If you want to change * a gradient, or colorize an image, use the applyColor method instead. * * You must pass the color with the alpha channel (usually, 0xFF). * For example, to change a red to green, use from=0xFFFF0000 (0xFF0000 with alpha=0xFF), to=0xFF00FF00. * * @see #applyColor(int) * @see #applyColor2(int) */ final public void changeColors(int from, int to) { int[] pixels = (int[]) (frameCount == 1 ? this.pixels : this.pixelsOfAllFrames); for (int n = pixels.length; --n >= 0;) if (pixels[n] == from) pixels[n] = to; if (frameCount != 1) {currentFrame = 2; setCurrentFrame(0);} } /** Saves this image as a Windows .png file format to the given PDBFile. * <ul> * <li>The stored image size is limited to near 64Kb. Note that a stored image size has no * relation to its size in pixels. For example, a 1300x1200 completely-white PNG file takes 7Kb * of storage size but 6MB of RAM when loaded. * <li>The PDBFile can save multiple images, but the record must * be prefixed with the image's name and must be sorted. * <li>This method finds the exact place where to insert the png and puts it there. * <li>If you want to create a png to be transfered by a stream to serial or socket * then you must use the method createPng instead. * <li>If a record with this name already exists, it will be replaced. * <li>The name is always converted to lowercase and the method makes sure that * .png is appended to it. * <li>To get the list of images in a PDBFile, just do a readString at the beginning of * each record. * <li>To retrieve the image, use loadFrom method. * </ul> * <p>Here is a sample code: * <pre> * // create the image and paint over it * Image img = new Image(100,100); * Graphics g = img.getGraphics(); * g.backColor = Color.getRGB(100,150,200); * g.fillRect(25,25,50,50); * g.foreColor = Color.WHITE; * g.drawCircle(50,50,20); * // create the PDBFile to save the image. You must change CRTR to match your apps creator ID * String pdbName = "images.CRTR.TYPE"; * PDBFile pdb = new PDBFile(pdbName, PDBFile.CREATE); * img.saveTo(pdb, "boxcircle.png"); * pdb.close(); * // load the previously created image * PDBFile pdb = new PDBFile(pdbName, PDBFile.READ_WRITE); * add(new ImageControl(Image.loadFrom(pdb,"boxcircle.png")),CENTER,CENTER); * pdb.close(); * </pre> * Here's a code that lists the images in a PDB (saved using this method). * <pre> * public static String[] list(PDBFile cat) throws IOException * { * DataStream ds = new DataStream(cat); * int n = cat.getRecordCount(); * String[] names = new String[n]; * for (int i =0; i < n; i++) * { * cat.setRecordPos(i); * names[i] = ds.readString(); * } * return names; * } * </pre> * @see #createPng(totalcross.io.Stream) * @see #loadFrom(PDBFile, String) */ public void saveTo(PDBFile cat, String name) throws ImageException, IOException { name = name.toLowerCase(); if (!name.endsWith(".png")) name += ".png"; int index = findPosition(cat, name, true); if (index == -1) index = cat.getRecordCount(); ResizeRecord rs = new ResizeRecord(cat,Math.min(65500, width*height*3+200)); // guich@tc114_17: make sure is not bigger than 64k DataStream ds = new DataStream(rs); rs.startRecord(index); ds.writeString(name); // write the name createPng(rs); rs.endRecord(); } /** Loads an image from a PDB file, if it was previously saved using saveTo method. * @see #saveTo(PDBFile, String) * @since TotalCross 1.22 */ public static Image loadFrom(PDBFile cat, String name) throws IOException, ImageException { name = name.toLowerCase(); if (!name.endsWith(".png")) name += ".png"; int idx = findPosition(cat, name, false); if (idx == -1) throw new IOException("The image "+name+" is not inside "+cat.getName()); cat.setRecordPos(idx); DataStream ds = new DataStream(cat); cat.skipBytes(ds.readUnsignedShort()); Image img = new Image(cat); cat.setRecordPos(-1); return img; } private static int findPosition(PDBFile cat, String name, boolean isWrite) throws IOException { DataStream ds = new DataStream(cat); // guich@200b4_45: fixed the insert_in_order routine int n = cat.getRecordCount(); for (int i =0; i < n; i++) // find the correct position to insert the record. the records must be sorted { cat.setRecordPos(i); String recName = ds.readString(); if (recName.compareTo(name) >= 0) // is recName greater than name { if (isWrite && name.equals(recName)) // same name? delete it cat.deleteRecord(); return i; } } return -1; } /** * Saves this image as a jpeg file to the given stream.<br> * NOT supported on Blackberry. * * @param s The output stream used to write the jpeg. * @param quality The quality of the image; 100 = no compression, 90 = medium compression, * 80 = high compression. Anything below 80 may greatly redude the image's quality. 85 is a common value. In JavaSE, the quality argument is ignored. * @throws ImageException * @throws IOException */ public void createJpg(Stream s, int quality) throws ImageException, IOException { try { java.awt.image.MemoryImageSource screenMis = new java.awt.image.MemoryImageSource(width, height, new java.awt.image.DirectColorModel(32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0), (int[])pixels, 0, width); screenMis.setAnimated(true); screenMis.setFullBufferUpdates(true); java.awt.Image screenImg = java.awt.Toolkit.getDefaultToolkit().createImage(screenMis); screenMis.newPixels(); java.awt.image.BufferedImage dest = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_RGB); java.awt.Graphics2D g2 = dest.createGraphics(); g2.drawImage(screenImg, 0, 0, null); g2.dispose(); java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(width); javax.imageio.ImageIO.write(dest, "jpg", bos); s.writeBytes(bos.toByteArray()); } catch (Throwable e) { throw new IOException(e.getMessage()); } } public void createJpg4B(Stream s, int quality) throws ImageException, IOException { } /** Saves this image as a 24 BPP .png file format (if useAlpha is true, it saves as 32 BPP), * to the given stream. * If you're sending the png through a stream but not saving to a PDBFile, * you can use this method. If you're going to save it to a PDBFile, then * you must use the saveTo method. * @throws ImageException * @throws IOException * @see #saveTo(totalcross.io.PDBFile, java.lang.String) */ public void createPng(Stream s) throws ImageException, IOException { try { // based in a code from J. David Eisenberg of PngEncoder, version 1.5 byte[] pngIdBytes = {(byte)-119, (byte)80, (byte)78, (byte)71, (byte)13, (byte)10, (byte)26, (byte)10}; CRC32Stream crc = new CRC32Stream(s); DataStream ds = new DataStream(crc); int w = frameCount > 1 ? this.widthOfAllFrames : this.width; int h = this.height; ds.writeBytes(pngIdBytes); // write the header ds.writeInt(13); crc.reset(); ds.writeBytes("IHDR".getBytes()); ds.writeInt(w); ds.writeInt(h); ds.writeByte(8); // bit depth of each rgb component ds.writeByte(6); // alpha or direct model ds.writeByte(0); // compression method ds.writeByte(0); // filter method ds.writeByte(0); // no interlace int c = (int)crc.getValue(); ds.writeInt(c); // write transparent pixel information, if any if (comment != null && comment.length() > 0) { ds.writeInt("Comment".length() + 1 + comment.length()); crc.reset(); ds.writeBytes("tEXt".getBytes()); ds.writeBytes("Comment".getBytes()); ds.writeByte(0); ds.writeBytes(comment.getBytes()); ds.writeInt((int)crc.getValue()); } // write the image data crc.reset(); final int bytesPerPixel = 4; byte[] row = new byte[bytesPerPixel * w]; byte[] filterType = new byte[1]; ByteArrayStream databas = new ByteArrayStream(bytesPerPixel * w * h + h); for (int y = 0; y < h; y++) { getPixelRow(row, y); databas.writeBytes(filterType,0,1); databas.writeBytes(row,0,row.length); } databas.mark(); ByteArrayStream compressed = new ByteArrayStream(w*h+h); int ncomp = ZLib.deflate(databas, compressed, -1); ds.writeInt(ncomp); crc.reset(); ds.writeBytes("IDAT".getBytes()); ds.writeBytes(compressed.getBuffer(), 0, ncomp); c = (int)crc.getValue(); ds.writeInt(c); // write the footer ds.writeInt(0); crc.reset(); ds.writeBytes("IEND".getBytes()); ds.writeInt((int)crc.getValue()); } catch (OutOfMemoryError oome) { throw new ImageException(oome.getMessage()+""); } } /** Used in saveTo method. Fills in the y row into the fillIn array. * there must be enough space for the full line be filled, with width*4 bytes. * The alpha channel is NOT stripped off. */ final public void getPixelRow(byte []fillIn, int y) { int[] row = (int[]) (frameCount > 1 ? this.pixelsOfAllFrames : this.pixels); int w = frameCount > 1 ? this.widthOfAllFrames : this.width; for (int x=0,n=w,i=y*w; n-- > 0;) { int p = row[i++]; fillIn[x++] = (byte)((p >> 16) & 0xFF); // r fillIn[x++] = (byte)((p >> 8) & 0xFF); // g fillIn[x++] = (byte)(p & 0xFF); // b fillIn[x++] = (byte)((p >>> 24) & 0xFF); // a } } /** * Returns the scaled instance for this image. The algorithm used is the replicate scale: not good quality, but fast. * * @since SuperWaba 3.5 */ public Image getScaledInstance(int newWidth, int newHeight) throws ImageException // guich@350_22 { // Based on the ImageProcessor class on "KickAss Java Programming" (Tonny Espeset) newWidth *= frameCount; // guich@tc100b5_40 Image scaledImage = getCopy(newWidth, newHeight); int[] dstImageData = (int[]) scaledImage.pixels; int[] srcImageData = (int[]) ((frameCount == 1) ? this.pixels : this.pixelsOfAllFrames); // guich@tc100b5_40 int fw = frameCount == 1 ? this.width : this.widthOfAllFrames; // guich@tc100b5_40 // guich: a modified version of the replicate scale algorithm. int h = newHeight << 1; int hi = this.height << 1; int hf = this.height / h; int wf = 0; int w = newWidth << 1; int wi = fw << 1; for (int y = 0; y < newHeight; y++, hf += hi) { wf = fw / w; int dstImage = y*newWidth; int srcImage = (hf / h) * fw; for (int x = newWidth; x > 0; x--, wf += wi) dstImageData[dstImage++] = srcImageData[srcImage + wf / w]; } if (frameCount > 1) // guich@tc100b5_40 scaledImage.setFrameCount(frameCount); return scaledImage; } private static final int BIAS_BITS = 16; private static final int BIAS = (1<<BIAS_BITS); /** Returns the scaled instance using the area averaging algorithm for this image. * Example: <pre> * Image img2 = img.getSmoothScaledInstance(200,200); * </pre> * In device and JavaSE it uses a Catmull-rom resampling, and in Blackberry it uses an area-average resampling. * The reason is that the Catmull-rom consumes more memory and is also slower than the area-average, although the * final result is much better. * @since TotalCross 1.0 */ public Image getSmoothScaledInstance(int newWidth, int newHeight) throws ImageException // guich@350_22 { // image preparation if (newWidth==width && newHeight==height) return this; newWidth *= frameCount; Image scaledImage = getCopy(newWidth, newHeight); int width = this.width * frameCount; int height = this.height; int[] pixels = (int[]) (frameCount==1 ? this.pixels : this.pixelsOfAllFrames); int[] pixels2= (int[]) scaledImage.pixels; // algorithm start int i, j, n; double xScale, yScale; int a,r,g,b; // Temporary values int val; int []v_weight; // Weight contribution [ow][MAX_CONTRIBS] int []v_pixel; // Pixel that contributes [ow][MAX_CONTRIBS] int []v_count; // How many contribution for the pixel [ow] int []v_wsum; // Sum of weights [ow] int []tb; // Temporary (intermediate buffer) double center; // Center of current sampling double weight; // Current wight int left; // Left of current sampling int right; // Right of current sampling int p_weight; // Temporary pointer int p_pixel; // Temporary pointer int maxContribs,maxContribsXY; // Almost-const: max number of contribution for current sampling double scaledRadius,scaledRadiusY; // Almost-const: scaled radius for downsampling operations double filterFactor; // Almost-const: filter factor for downsampling operations /* Aliasing buffers */ xScale = ((double)newWidth / width); yScale = ((double)newHeight / height); if (xScale > 1.0) { /* Horizontal upsampling */ filterFactor = 1; scaledRadius = 2; } else { /* Horizontal downsampling */ filterFactor = xScale; scaledRadius = 2 / xScale; } maxContribs = (int) (2 * scaledRadius + 1); scaledRadiusY = yScale > 1.0 ? 2 : 2 / yScale; maxContribsXY = (int) (2 * Math.max(scaledRadiusY,scaledRadius) + 1); /* Pre-allocating all of the needed memory */ int s = newWidth > newHeight ? newWidth : newHeight; try { tb = new int[newWidth * height]; v_weight = new int[s * maxContribsXY]; /* weights */ v_pixel = new int[s * maxContribsXY]; /* the contributing pixels */ v_count = new int[s]; /* how may contributions for the target pixel */ v_wsum = new int[s]; /* sum of the weights for the target pixel */ } catch (OutOfMemoryError t) { throw new ImageException("Out of memory"); } /* Pre-calculate weights contribution for a row */ for (i = 0; i < newWidth; i++) { p_weight = i * maxContribs; p_pixel = i * maxContribs; v_count[i] = 0; v_wsum[i] = 0; center = ((double)i)/xScale; left = (int)(center + 0.5 - scaledRadius); right = (int)(left + 2 * scaledRadius); for (j = left; j <= right; j++) { if (j < 0 || j >= width) continue; // Catmull-rom resampling double cc = (center-j) * filterFactor; if (cc < 0.0) cc = - cc; if (cc <= 1.0) weight = 1.5f * cc * cc * cc - 2.5f * cc * cc + 1; else if (cc <= 2.0) weight = -0.5f * cc * cc * cc + 2.5f * cc * cc - 4 * cc + 2; else continue; if (weight == 0) continue; int iweight = (int)(weight * BIAS); n = v_count[i]; /* Since v_count[i] is our current index */ v_pixel[p_pixel+n] = j; v_weight[p_weight+n] = iweight; v_wsum[i] += iweight; v_count[i]++; /* Increment contribution count */ } } /* Filter horizontally from input to temporary buffer */ for ( i = 0; i < newWidth; i++) { int count = v_count[i]; int wsum = v_wsum[i]; /* Here 'n' runs on the vertical coordinate */ for (n = 0; n < height; n++) { /* i runs on the horizontal coordinate */ p_weight = i * maxContribs; p_pixel = i * maxContribs; val = a = r = g = b = 0; for (j=0; j < count; j++) { int iweight = v_weight[p_weight++]; val = pixels[v_pixel[p_pixel++] + n * width]; /* Using val as temporary storage */ /* Acting on color components */ a += ((val>>24)&0xFF) * iweight; r += ((val>>16)&0xFF) * iweight; g += ((val>> 8)&0xFF) * iweight; b += ((val )&0xFF) * iweight; } a /= wsum; if (a > 255) a = 255; else if (a < 0) a = 0; r /= wsum; if (r > 255) r = 255; else if (r < 0) r = 0; g /= wsum; if (g > 255) g = 255; else if (g < 0) g = 0; b /= wsum; if (b > 255) b = 255; else if (b < 0) b = 0; tb[i+n*newWidth] = (a<<24) | (r << 16) | (g << 8) | b; /* Temporary buffer */ } } /* Going to vertical stuff */ if ( yScale > 1.0) { filterFactor = 1; scaledRadius = 2; } else { filterFactor = yScale; scaledRadius = 2 / yScale; } maxContribs = (int) (2 * scaledRadius + 1); /* Pre-calculate filter contributions for a column */ for (i = v_weight.length; --i >= 0;) v_weight[i] = v_pixel[i] = 0; for (i = 0; i < newHeight; i++) { p_weight = i * maxContribs; p_pixel = i * maxContribs; v_count[i] = 0; v_wsum[i] = 0; center = ((double) i) / yScale; left = (int) (center+0.5 - scaledRadius); right = (int)( left + 2 * scaledRadius); for (j = left; j <= right; j++) { if (j < 0 || j >= height) continue; // Catmull-rom resampling double cc = (center-j) * filterFactor; if (cc < 0.0) cc = -cc; if (cc <= 1.0) weight = 1.5f * cc * cc * cc - 2.5f * cc * cc + 1; else if (cc <= 2.0) weight = -0.5f * cc * cc * cc + 2.5f * cc * cc - 4 * cc + 2; else continue; if (weight == 0) continue; int iweight = (int)(weight * BIAS); n = v_count[i]; /* Our current index */ v_pixel[p_pixel+n] = j; v_weight[p_weight+n] = iweight; v_wsum[i]+= iweight; v_count[i]++; /* Increment the contribution count */ } } int idx = 0; /* Filter vertically from work to output */ for (i = 0; i < newHeight; i++) { int count = v_count[i]; int wsum = v_wsum[i]; for (n = 0; n < newWidth; n++) { p_weight = i * maxContribs; p_pixel = i * maxContribs; val = a = r = g = b = 0; for (j = 0; j < count; j++) { int iweight = v_weight[p_weight++]; val = tb[ n + newWidth * v_pixel[p_pixel++]]; /* Using val as temporary storage */ /* Acting on color components */ a += ((val>>24)&0xFF) * iweight; r += ((val>>16)&0xFF) * iweight; g += ((val>> 8)&0xFF) * iweight; b += ((val )&0xFF) * iweight; } if (wsum == 0) continue; a /= wsum; if (a > 255) a = 255; else if (a < 0) a = 0; r /= wsum; if (r > 255) r = 255; else if (r < 0) r = 0; g /= wsum; if (g > 255) g = 255; else if (g < 0) g = 0; b /= wsum; if (b > 255) b = 255; else if (b < 0) b = 0; pixels2[idx++] = (a<<24) | (r << 16) | (g << 8) | b; } } if (frameCount > 1) // guich@tc100b5_40 scaledImage.setFrameCount(frameCount); return scaledImage; } /** Returns the scaled instance for this image, given the scale arguments. The algorithm used is * the replicate scale, not good quality, but fast. Given values must be > 0. * @since SuperWaba 4.1 */ public Image scaledBy(double scaleX, double scaleY) throws ImageException // guich@402_6 { return ((scaleX == 1 && scaleY == 1) || scaleX <= 0 || scaleY <= 0)?this:getScaledInstance((int)(width*scaleX), (int)(height*scaleY)); // guich@400_23: now test if the width/height are the same, what returns the original image } /** Returns the scaled instance for this image, given the scale arguments. Given values must be > 0. * The backColor replaces the transparent pixel of the current image to produce a smooth border. * Example: <pre> * Image img2 = img.smoothScaledBy(0.75,0.75, getBackColor()); * </pre> * @since TotalCross 1.0 */ public Image smoothScaledBy(double scaleX, double scaleY) throws ImageException // guich@402_6 { return ((scaleX == 1 && scaleY == 1) || scaleX <= 0 || scaleY <= 0)?this:getSmoothScaledInstance((int)(width*scaleX), (int)(height*scaleY)); // guich@400_23: now test if the width/height are the same, what returns the original image } /** Returns the scaled instance using fixed aspect ratio for this image, given the scale arguments. Given values must be > 0. * This method is useful to resize an image, specifying only one of its sides: the width or the height. The other side * is computed to keep the aspect ratio. * * @param newSize The new size (width or height) for the image * @param isHeight If true, the newSize is considered as the new height of the image. If false, the newSize is considered the new width of the image. * * Example: <pre> * Image img2 = img.smoothScaledFixed(fmH, true, -1); * </pre> * @since TotalCross 1.53 */ public Image smoothScaledFixedAspectRatio(int newSize, boolean isHeight) throws ImageException // guich@402_6 { int w = !isHeight ? newSize : (newSize * width / height); int h = isHeight ? newSize : (newSize * height / width); return getSmoothScaledInstance(w, h); } /** Creates a rotated and/or scaled version of this Image. * A new <code>Image</code> object is returned which will render * the image at the specified <code>scale</code> ratio and * rotation <code>angle</code>.  * After rotation, the empty parts of the rectangular area of * the resulting image are filled with the <code>fill</code> color. * If <code>color</code> is <code><i>-1</i></code>, then * the fill color is the transparent color, or white if none. * <p>Notes * <ul> * <li> the new image will probably have a different size of this image. * <li> if you want just to scale, use the getScaledInstance or * scaleBy (and the smooth ones) instead, because they are faster. * <li> If you need a smooth rotate and scale, scale it first with * getScaledInstance then rotate without scale (or vice-versa) * <li> In multiframe images, each image is rotated/scaled independently. * </ul> * @param scale a number greater than or equal to 0 stating the percentage * of scaling to be performed.  * 100 is not scaling, 200 doubles the size, 50 shrinks the image by 2 * @param angle the rotation angle, expressed in trigonometric degrees * @param fillColor the fill color; -1 indicates the transparent color of this image or * Color.WHITE if the transparentColor was not set; use 0 for a transparent background, or 0xFF000000 for the BLACK color. */ public Image getRotatedScaledInstance(int scale, int angle, int fillColor) throws ImageException { if (scale <= 0) scale = 1; /* xplying by 0x10000 allow integer math, while not loosing much prec. */ int rawSine=0; int rawCosine=0; int sine=0; int cosine=0; angle = angle % 360; if ((angle % 90) == 0) { if (angle < 0) angle += 360; switch (angle) { case 0: rawCosine = 0x10000; cosine = 0x640000 / scale; break; case 90: rawSine = 0x10000; sine = 0x640000 / scale; break; case 180: rawCosine = -0x10000; cosine = -0x640000 / scale; break; default: // case 270: rawSine = -0x10000; sine = -0x640000 / scale; break; } } else { double rad = angle * 0.0174532925; rawSine = (int) (Math.sin(rad) * 0x10000); rawCosine = (int) (Math.cos(rad) * 0x10000); sine = (rawSine * 100) / scale; cosine = (rawCosine * 100) / scale; } int hIn = this.height; int wIn = this.width; /* create imageOut */ int cornersX[] = new int[3]; int cornersY[] = new int[3]; int xMin = 0; int yMin = 0; int xMax = 0; int yMax = 0; cornersX[0] = (wIn * rawCosine) >> 16; cornersY[0] = (wIn * rawSine) >> 16; cornersX[2] = (-hIn * rawSine) >> 16; cornersY[2] = (hIn * rawCosine) >> 16; cornersX[1] = cornersX[0] + cornersX[2]; cornersY[1] = cornersY[0] + cornersY[2]; for (int i = 2; --i >= 0;) { if (cornersX[i] < xMin) xMin = cornersX[i]; else if (cornersX[i] > xMax) xMax = cornersX[i]; if (cornersY[i] < yMin) yMin = cornersY[i]; else if (cornersY[i] > yMax) yMax = cornersY[i]; } if (width == height) { xMax = yMax = width; xMin = yMin = 0; } int wOut = ((xMax - xMin) * scale) / 100; int hOut = ((yMax - yMin) * scale) / 100; Image imageOut = getCopy(wOut * frameCount, hOut); if (frameCount > 1) imageOut.setFrameCount(frameCount); for (int f = 0; f < frameCount; f++) { if (frameCount != 1) { setCurrentFrame(f); imageOut.setCurrentFrame(f); } int[] pixelsIn = (int[]) this.pixels; /* center */ int x0 = ((wIn << 16) - (((xMax - xMin) * rawCosine) - ((yMax - yMin) * rawSine)) - 1) / 2; int y0 = ((hIn << 16) - (((xMax - xMin) * rawSine) + ((yMax - yMin) * rawCosine)) - 1) / 2; /* and draw! */ int[] lineOut = (int[]) imageOut.pixels; for (int l = 0; l < hOut; l++) { int x = x0; int y = y0; int iOut = l * imageOut.width; for (int i = wOut; --i >= 0; x += cosine, y += sine) { int u = x >> 16; int v = y >> 16; if (0 <= u && u < wIn && 0 <= v && v < hIn) lineOut[iOut++] = pixelsIn[v*this.width+u]; else lineOut[iOut++] = fillColor; } x0 -= sine; y0 += cosine; } if (frameCount != 1) for (int y = imageOut.height-1; y >= 0; y--) Vm.arrayCopy(imageOut.pixels, y * imageOut.width, imageOut.pixelsOfAllFrames, f * imageOut.width + y * imageOut.widthOfAllFrames, imageOut.width); } if (frameCount != 1) { setCurrentFrame(0); imageOut.setCurrentFrame(0); } return imageOut; // success } /** Creates a faded instance of this image, interpolating all pixels with the given background color. * @deprecated Use getFadedInstance() instead * @see #getFadedInstance() * @since TotalCross 1.01 */ public Image getFadedInstance(int backColor) throws ImageException // guich@tc110_50 { Image imageOut = getCopy(frameCount > 1 ? widthOfAllFrames : width, height); if (frameCount > 1) imageOut.setFrameCount(frameCount); int[] from = (int[])(frameCount > 1 ? pixelsOfAllFrames : pixels); int[] to = (int[])(frameCount > 1 ? imageOut.pixelsOfAllFrames : imageOut.pixels); for (int i = from.length; --i >= 0;) to[i] = (from[i] & 0xFF000000) | Color.interpolate(backColor,from[i]); // keep the alpha channel unchanged if (frameCount != 1) { imageOut.currentFrame = -1; imageOut.setCurrentFrame(0); } return imageOut; } private Image getCopy(int w, int h) throws ImageException { Image i = new Image(w,h); // copy other attributes return i; } /** Used in getFadedInstance(). */ public static int FADE_VALUE = -96; /** Creates a faded instance of this image, decreasing the alpha-channel by 128 for all pixels. * @since TotalCross 2.0 * @see #FADE_VALUE */ public Image getFadedInstance() throws ImageException // guich@tc110_50 { return getAlphaInstance(FADE_VALUE); } /** Adds the given value to each pixel's alpha-channel of this image. * Only the pixels that don't have a 0 alpha are changed. * @since TotalCross 2.0 */ public Image getAlphaInstance(int delta) throws ImageException { Image imageOut = getCopy(frameCount > 1 ? widthOfAllFrames : width, height); if (frameCount > 1) imageOut.setFrameCount(frameCount); int[] from = (int[])(frameCount > 1 ? pixelsOfAllFrames : pixels); int[] to = (int[])(frameCount > 1 ? imageOut.pixelsOfAllFrames : imageOut.pixels); for (int i = from.length; --i >= 0;) { int p = from[i]; if ((p & 0xFF000000) == 0) to[i] = p; else { int a = (p >>> 24) & 0xFF; a += delta; if (a < 0) a = 0; else if (a > 255) a = 255; to[i] = (p & 0x00FFFFFF) | (a << 24); } } if (frameCount != 1) { imageOut.currentFrame = -1; imageOut.setCurrentFrame(0); } return imageOut; } /** * Creates a touched-up version of this Image with the specified brightness and contrast. A new <code>Image</code> * object is returned which will render the image at the specified <code>brigthness</code>and the specified * <code>contrast</code>. * * @param brightness * a number between -128 and 127 stating the desired level of brightness.  127 is the highest * brightness level (white image), -128 is no brightness (darkest image). * @param contrast * a number between -128 and 127 stating the desired level of contrast.  127 is the highest contrast * level, -128 is no contrast. */ public Image getTouchedUpInstance(byte brightness, byte contrast) throws ImageException { final int NO_TOUCHUP = 0; final int BRITE_TOUCHUP = 1; final int CONTRAST_TOUCHUP = 2; int touchup = NO_TOUCHUP; int[] pixelsIn = (int[]) (frameCount == 1 ? this.pixels : this.pixelsOfAllFrames); int w = frameCount == 1 ? this.width : this.widthOfAllFrames; int h = this.height; Image imageOut = getCopy(w, h); int[] pixelsOut = (int[]) imageOut.pixels; short table[] = null; int m = 0, k = 0; if (contrast != 0) { touchup |= CONTRAST_TOUCHUP; table = computeContrastTable(contrast); } if (brightness != 0) { touchup |= BRITE_TOUCHUP; double eBrightness = (brightness + 128.0) / 128.0; // [0.0 ... 2.0] if (brightness <= 1.0) { m = (int) (Math.sqrt(eBrightness) * 0x10000); k = 0; } else { double f = eBrightness - 1.0; f = f * f; k = (int) (f * 0xFF0000); m = (int) ((1.0 - f) * eBrightness * 0x10000); } } // no palette int in[] = pixelsIn; int out[] = pixelsOut; switch (touchup) { case NO_TOUCHUP: Vm.arrayCopy(in, 0, out, 0, w*h); break; case BRITE_TOUCHUP: for (int i = w*h-1; i >= 0; i--) { int p = in[i]; int a = p & 0xFF000000; int r = (p >> 16) & 0xFF; int g = (p >> 8) & 0xFF; int b = p & 0xFF; out[i] = a | Color.getRGBEnsureRange(((m * r) + k) >> 16, ((m * g) + k) >> 16, ((m * b) + k) >> 16); } break; case CONTRAST_TOUCHUP: for (int i = w*h-1; i >= 0; i--) { int p = in[i]; int a = p & 0xFF000000; int r = (p >> 16) & 0xFF; int g = (p >> 8) & 0xFF; int b = p & 0xFF; out[i] = a | Color.getRGBEnsureRange(table[r], table[g], table[b] & 0xFF); } break; default: // case CTRSTBRITE_TOUCHUP: for (int i = w*h-1; i >= 0; i--) { int p = in[i]; int a = p & 0xFF000000; int r = table[(p >> 16) & 0xFF]; int g = table[(p >> 8) & 0xFF]; int b = table[p & 0xFF]; out[i] = a | Color.getRGBEnsureRange(((m * r) + k) >> 16, ((m * g) + k) >> 16, ((m * b) + k) >> 16); } break; } if (frameCount > 1) // guich@tc100b5_40 imageOut.setFrameCount(frameCount); return imageOut; } /** Internal use only. */ private void copyFrom(Image img) { this.width = img.getWidth(); this.height = img.getHeight(); this.pixels = img.pixels; this.frameCount = img.frameCount; this.comment = img.comment; } private short[] computeContrastTable(byte level) { double factor; short[] table = new short[256]; if (level < 0) // byte ranges -128 to +127 factor = (level+128) / 128.0; else factor = 127.0 / Math.max(127 - level,1); for (int i = 0; i <= 127; i++) { int v = ((int) (127.0 * Math.pow(i / 127.0, factor))) & 0xff; table[i] = (short)v; table[255 - i] = (short) (255 - v); } return table; } private void imageLoad(String path) throws ImageException { byte[] bytes = Launcher.instance.readBytes(path); // NOTE: we could use the following to read out of an applet's JAR file // if we could get a pathObject which was in the root directory (the // App for example). However, if we don't have one. If we loaded an // image in the App constructor we wouldn't have the App object yet... // if (!isApp) // try // { // Object pathObject = Applet.currentApplet; // stream = pathObject.getClass().getResourceAsStream(path); // } // catch (Exception e) {}; if (bytes == null) throw new ImageException("ERROR: can't open image file " + path); if (new String(bytes, 0, 2).equals("BM")) ImageLoadBMPCompressed(bytes, bytes.length); else imageLoad(bytes, bytes.length); } private void imageParse(byte[] fullBmpDescription, int length) throws ImageException { if (new String(fullBmpDescription, 0, 2).equals("BM")) ImageLoadBMPCompressed(fullBmpDescription, length); else imageLoad(fullBmpDescription, length); } // ///////////////// METHODS TAKEN FROM THE TOTALCROSS VM //////////////////// // ////////////////////////////////////////////////////////////////////////// /* * Floyd/Steinberg error diffusion dithering algorithm in color. The array * line[][] contains the RGB values for the * current line being processed; * line[0][x] = red, line[1][x] = green, line[2][x] = blue. */ // ps: this algorithm was heavily modified and optimized by guich private int[] colorTable; // readRGB reads in pixels values that are stored uncompressed. // The bits represent indices into the color table. private void readRGB(int width, int height, int bpp, byte[] in, int offset) { //totalcross.JavaBridge.print("reading " + (doDither ? "and dithering " : "") + "rgb " + bpp + "bpp"); // How many pixels can be stored in a byte? int pixelsPerByte = 8 / bpp; // A bit mask containing the number of bits in a pixel int bitMask = (1 << bpp) - 1; int bitShifts[] = new int[8]; int i, x, y, row=0; int whichBit = 0; int currByte; int div = 32 / bpp; // The shift values that will move each pixel to the far right for (i = 0; i < pixelsPerByte; i++) bitShifts[i] = 8 - ((i + 1) * bpp); int[] pix = (int[]) this.pixels; int pitch = ((width + div - 1) / div) * div; // make sure are in a 4 byte boundary - those extra pixels will be stripped off by the current clip int dif = pitch - width; // Start at the bottom of the pixel array and work up switch (bpp) { case 16: // guich@tc111_1 pitch = (width*2+3) & ~3; // guich@tc114_30: bmp with w=41 has 84 bytes per row for (dif = pitch - width*2, y=height-1, row = y * width; y >= 0; y--, offset += dif, row -= width+width) // guich@tc110_107 for (x=width; x > 0; x--) { int pixel = (in[offset++] & 0xFF) | ((in[offset++] & 0xFF) << 8); int r = (pixel >> 10) & 0x1f; int g = (pixel >> 5) & 0x1f; int b = pixel & 0x1f; pix[row++] = 0xFF000000 | (r << 19) | (g << 11) | (b << 3); } break; case 32: // guich@tc114_15 for (y=height-1, row = y * width; y >= 0; y--, row -= width+width) for (x=width; x > 0; x--) pix[row++] = 0xFF000000 | ((in[offset++] & 0xFF) /*<< 0*/) | ((in[offset++] & 0xFF) << 8) | ((in[offset++] & 0xFF) << 16) | ((in[offset++] & 0xFF) << 24); break; case 24: pitch = (width*3+3) & ~3; // guich@tc110_107: must consider the width in bytes, not in pixels for (dif = pitch - width*3, y=height-1, row = y * width; y >= 0; y--, offset += dif, row -= width+width) // guich@tc110_107 for (x=width; x > 0; x--) pix[row++] = 0xFF000000 | (((in[offset++] & 0xFF) /*<< 0*/) | ((in[offset++] & 0xFF) << 8) | ((in[offset++] & 0xFF) << 16)); // guich@tc114:20: fixed order break; case 8: // guich@200b3: if 8bpp, use a faster routine for (y=height-1, row = y * width; y >= 0; y--, offset += dif, row -= width+width) for (x=width; x > 0; x--) pix[row++] = 0xFF000000 | colorTable[in[offset++] & 0xFF]; break; default: // Read in the first byte currByte = in[offset++] & 0xFF; // Start at the bottom of the pixel array and work up for (y = height-1, row = y * width; y >= 0; y--, row -= width+width) for (x = 0; x < pitch; x++) { // Get the next pixel from the current byte if (x < width) pix[row++] = 0xFF000000 | colorTable[(currByte >> bitShifts[whichBit]) & bitMask]; // If the current bit position is past the number of pixels in a byte, advance to the next byte if (++whichBit >= pixelsPerByte) { whichBit = 0; if (offset < in.length) currByte = in[offset++] & 0xFF; } } break; } } private void readRLE(int width, int height, byte[] in, int offset, boolean rle8) { int val; int len, esc, r; int x, y; int colors0 = 0, colors1 = 0; int []pix = (int[]) this.pixels; x = 0; y = height - 1; while (true) { esc = in[offset++] & 0xFF; // encoded mode starts with a run length, and then a byte with two colour indexes to alternate between for the // run if (esc != 0) { if (rle8) { colors0 = colorTable[in[offset++] & 0xFF]; for (r = y * width +x; esc-- > 0; x++, r++) if (x < width) pix[r] = colors0; } else { val = in[offset++] & 0xFF; colors0 = colorTable[(val >> 4) & 0x0f]; colors1 = colorTable[val & 0x0f]; for (len = 1, r = y * width + x; len <= esc; len++, r++, x++) if (x < width) pix[r] = ((len & 1) == 1) ? colors0 : colors1; // odd count, low nybble } } else // A leading zero is an escape; it may signal the end of the bitmap, a cursor move, or some absolute data. { // zero tag may be absolute mode or an escape esc = in[offset++] & 0xFF; switch (esc) { case 0: // end of line x = 0; y--; break; case 1: // end of bitmap return; case 2: // delta x += in[offset++] & 0xFF; y -= in[offset++] & 0xFF; break; default: // no compression if (rle8) { len = esc; for (r = y * width + x; len-- > 0; r++, x++, offset++) if (x < width) pix[r] = colorTable[in[offset] & 0xFF]; if ((esc & 1) != 0) // Must have even nunber of bytes offset++; } else // guich@421_6: fixed this algorithm. { for (r = y * width + x, len = 1; len <= esc; len++, r++, x++) { if ((len & 1) == 1) { val = in[offset++] & 0xFF; colors0 = colorTable[(val >> 4) & 0x0f]; colors1 = colorTable[val & 0x0f]; if (x < width) pix[r] = colors0; } else if (x < width) pix[r] = colors1; // odd count, low nybble } if ((((esc + 1) >> 1) & 1) != 0) // Must have even nunber of words offset++; } break; } } } } // Intel architecture getUInt16 private int inGetUint16(byte bytes[], int off) { return ((bytes[off + 1] & 0xFF) << 8) | (bytes[off] & 0xFF); } // Intel architecture getUInt32 private int inGetUint(byte bytes[], int off) { return ((bytes[off + 3] & 0xFF) << 24) | ((bytes[off + 2] & 0xFF) << 16) | ((bytes[off + 1] & 0xFF) << 8) | (bytes[off] & 0xFF); } // created by guich to handle all types of modern bitmaps, private static final int BI_RGB = 0; private static final int BI_RLE8 = 1; private void ImageLoadBMPCompressed(byte[] p, int length) throws ImageException { int bitmapOffset, infoSize; int compression, usedColors; // header (54 bytes) // 0-1 magic chars 'BM' // 2-5 uint filesize (not reliable) // 6-7 uint16 0 // 8-9 uint16 0 // 10-13 uint bitmapOffset // 14-17 uint info size // 18-21 int width // 22-25 int height // 26-27 uint16 nplanes // 28-29 uint16 bits per pixel // 30-33 uint compression flag // 34-37 uint image size in bytes // 38-41 int biXPixelsPerMeter // 42-45 int biYPixelsPerMeter // 46-49 uint colors used // 50-53 uint important color count // 54- uchar bitmap bytes depending to type // Each scan line of image data is padded to the next four byte boundary if (p[0] != 'B' || p[1] != 'M') throw new ImageException("Error in Image: not a bmp file!"); bitmapOffset = inGetUint(p, 10); infoSize = inGetUint(p, 14); if (infoSize != 40) throw new ImageException("Error in Image: old style bmp"); this.width = inGetUint(p, 18); this.height = inGetUint(p, 22); if (this.width > 65535 || this.height > 65535 || this.width <= 0 || this.height <= 0) throw new ImageException("Error in Image: bad width/height"); int bmpBPP = inGetUint16(p, 28); compression = inGetUint(p, 30); /* imageSize = */inGetUint(p, 34); usedColors = inGetUint(p, 46); if (usedColors == 0 && bmpBPP <= 8) usedColors = 1 << bmpBPP; colorTable = bmpBPP >= 16 ? null : new int[1 << bmpBPP]; // guich@340_59: in some bitmaps, colorsUsed may be // smaller than the number of possible colors, but the bitmap may still // have indexes greater than the usedColors, thus making the bitmap // appear black. To avoid this, we always allocate 1<<BPP // Read the bitmap's color table for (int i = 0, j = 54; i < usedColors; i++, j += 4) colorTable[i] = (inGetUint(p, j) & 0xFFFFFF); // prepares the color table for this screen bpp. if (bmpBPP == 1 && colorTable[0] == 0xFFFFFF) // the bitmap is monochrome, are the colors iverted? { colorTable[1] = 0xFFFFFF; colorTable[0] = 0x000000; } /* * if (storePalette) { GRHANDLE_PaletteSize(imgHandle) = 256; GRHANDLE_Palette(imgHandle) = colorTable; } */ // Create space for the pixels this.pixels = new int[this.height*this.width]; // Read the pixels from the stream based on the compression type directly into the selected offscreen image if (compression == BI_RGB) readRGB(this.width, this.height, bmpBPP, p, bitmapOffset); else if (bmpBPP == 16) throw new ImageException("16-bpp BMP compressed RLE images is not supported! Use 24-bpp instead."); else readRLE(this.width, this.height, p, bitmapOffset, compression == BI_RLE8); setTransparentColor(Color.WHITE); // every bmp image has white as default transparent color } /** * Fill the required fields of the GifImage <code>image</code> from reading the stream <code>input</code> * * @param image * totalcross.ui.image.Image to construct * @param maxColors * maxColors for the device * @param isPalettized * true if indexing into a web safe palette is required * @param input * totalcross.io.Stream that contains the GIF encoded bytes of the image * @param buffer * array of bytes used as a working buffer - its size should be between 512 and 2048 bytes * @param imageNo * position of the image in a multi-image file must start (and default to) zero. */ private void imageLoad(byte[] input, int len) throws ImageException { try { ImageLoader loader = new ImageLoader(input,len); loader.load(this, 20000000); if (!loader.isSupported) throw new ImageException("TotalCross does not support grayscale+alpha PNG images. Save the image as color (24 bpp)."); } catch (InterruptedException ex) { throw new ImageException(ex.getMessage()); } } static class ImageLoader implements java.awt.image.ImageConsumer { private java.awt.image.ImageProducer producer; private int width, height; private Image imageCur; private boolean isImageComplete; private byte[] imgBytes; private boolean isGif; private Vector frames = new Vector(5); private java.awt.image.ColorModel colorModel; boolean isSupported = true; private int transparentColor = -3; private boolean useAlpha; /** * Create a ImageLoader object to grab frames from the image <code>img</code> * * @param input * the input stream where the image to retrieve the image data comes from */ public ImageLoader(byte[] input, int len) { this.imgBytes = input; this.isImageComplete = true; try { java.awt.Component component = new java.awt.Component() {}; java.awt.MediaTracker tracker = new java.awt.MediaTracker(component); java.awt.Image image = GraphicsEnvironment.isHeadless() ? ImageIO.read(new ByteArrayInputStream(input, 0, len)) : java.awt.Toolkit.getDefaultToolkit().createImage(input, 0, len); tracker.addImage(image, 0); tracker.waitForAll(); if (!tracker.isErrorAny()) { this.isImageComplete = false; this.producer = image.getSource(); this.width = -1; this.height = -1; } } catch (InterruptedException e) { } catch (java.io.IOException e) { // should never happen e.printStackTrace(); } } private void getPNGInformations(byte[] input, Image imgCur) // a shame that Java doesn't support retrieving the comments! { byte[] bytes = new byte[4]; int colorType = 0; try { ByteArrayStream bas = new ByteArrayStream(input); DataStream ds = new DataStream(bas); ds.skipBytes(8); int pltePos = -1; int plteLen = -1; while (true) { int len = ds.readInt(); ds.readBytes(bytes); String id = new String(bytes); if (id.equals("IHDR")) { ds.skipBytes(9); colorType = ds.readByte(); bas.skipBytes(-10); useAlpha = colorType == 4 || colorType == 6; isSupported = colorType != 4; } else if (id.equals("PLTE")) // guich@tc100b5_4 { pltePos = bas.getPos(); plteLen = len; } else if (id.equals("tRNS")) // guich@tc100b5_4 { switch (len) { case 6: // RGB transparentColor = Color.getRGBEnsureRange(ds.readUnsignedShort(),ds.readUnsignedShort(),ds.readUnsignedShort()); bas.skipBytes(-6); break; case 256: // palettized? find the color that is transparent (0) if (colorType == 3) // guich@tc130: palettized with alpha-channel palette useAlpha = true; for (int i = 0, pos = bas.getPos(); i < 256; i++,pos++) if (input[pos] == 0) { if (plteLen == 768) // RGB? transparentColor = Color.getRGB(input[pltePos+i*3] & 0xFF,input[pltePos+i*3+1] & 0xFF,input[pltePos+i*3+2] & 0xFF); break; } break; } } else if (id.equals("IEND")) break; else if (id.equals("tEXt")) { String type = ds.readCString(); if (type.equals("Comment")) { bytes = new byte[len-type.length()-1]; ds.readBytes(bytes); imageCur.comment = new String(bytes); } else bas.skipBytes(-type.length()-1); // guich@tc100b5_31: go back if its not our comment } ds.skipBytes(len+4); // skip data and crc } } catch (Exception e) {} } /** * Fill the fields of an empty image * * @param image * Image in which the fields need to be filled * @param millis * time out - if 0, it waits forever */ public synchronized void load(Image image, int millis) throws InterruptedException { Image loaded = load(millis); if (loaded != null) { int fc = loaded.frameCount; loaded.frameCount = 1; // guich@tc100b5: cannot be 0 image.copyFrom(loaded); if (fc > 0) image.comment = loaded.comment == null ? "FC="+fc : loaded.comment; if (!useAlpha && transparentColor != -3) image.setTransparentColor(transparentColor); } } /** * Create the array of Image * * @param millis * time out - if 0, it waits forever * @return an array of Image, an image per frame */ public synchronized Image load(int millis) throws InterruptedException { if (!isImageComplete) { int stopTime = millis + Vm.getTimeStamp(); producer.startProduction(this); while (!isImageComplete) { if (millis <= 0) { wait(0); } else { long remainTime = stopTime - Vm.getTimeStamp(); if (remainTime <= 0) { break; } wait(remainTime); } } } return imageCur; } public void setDimensions(int width, int height) { this.width = width; this.height = height; } public void setHints(int hints) { } @SuppressWarnings("rawtypes") public void setProperties(java.util.Hashtable props) { } public void setColorModel(java.awt.image.ColorModel model) { this.colorModel = model; } public final void setPixels(int x, int y, int w, int h, java.awt.image.ColorModel model, byte pixels[], int off, int scansize) { if (imageStarted()) { int p[] = (int[]) imageCur.pixels; int jMax = y + h; int iMax = x + w; if (useAlpha) for (int j = y; j < jMax; ++j, off += scansize) for (int i = j*width+x,ii=x, k = off; ii < iMax; ii++) p[i++] = model.getRGB(pixels[k++] & 0xFF); else for (int j = y; j < jMax; ++j, off += scansize) for (int i = j*width+x,ii=x, k = off; ii < iMax; ii++) p[i++] = model.getRGB(pixels[k++] & 0xFF) | 0xFF000000; } } public final void setPixels(int x, int y, int w, int h, java.awt.image.ColorModel model, int pixels[], int off, int scansize) { if (imageStarted()) { int[] p = (int[]) imageCur.pixels; int jMax = y + h; int iMax = x + w; if (useAlpha) for (int j = y; j < jMax; ++j, off += scansize) for (int i = j*width+x,ii=x, k = off; ii < iMax; ii++) p[i++] = model.getRGB(pixels[k++]); else for (int j = y; j < jMax; ++j, off += scansize) for (int i = j*width+x,ii=x, k = off; ii < iMax; ii++) p[i++] = model.getRGB(pixels[k++]) | 0xFF000000; } } /** * Create a new current Image if necessary * * @return true if the image was created, false otherwise. */ private final boolean imageStarted() { if (imageCur == null) { if (width < 0 || height < 0) return false; else { try { imageCur = new Image(width, height); } catch (ImageException e) { return false; } if (new String(imgBytes,1,3).equals("PNG")) getPNGInformations(imgBytes, imageCur); else if (new String(imgBytes,0,3).equals("GIF")) isGif = true; // int index; if (transparentColor == -3) // guich@tc130: not already changed? { if ((colorModel instanceof java.awt.image.IndexColorModel) && (-1 != (index = ((java.awt.image.IndexColorModel) colorModel).getTransparentPixel()))) transparentColor = colorModel.getRGB(index & 0xFF) & 0xFFFFFF; } if (transparentColor >= 0) { // fill all pixels with the transparent color int[] p = (int[])imageCur.pixels; Convert.fill(p, 0, p.length, transparentColor | 0xFF000000); } } } return true; } private boolean arraysEquals(int[] pix1, int[] pix2) { if (pix1.length != pix2.length) return false; try { return java.util.Arrays.equals(pix1,pix2); // jdk 1.2.x available? } catch (Throwable t) {} for (int i =0; i < pix1.length; i++) if (pix1[i] != pix2[i]) return false; return true; } private void joinImages() { int n = frames.size(); if (n == 1) // a single image? imageCur = (Image)frames.items[0]; else try { int totalW = 0; int totalH = imageCur.height; for (int i =0; i < n; i++) totalW += ((Image)frames.items[i]).width; Image temp = new Image(totalW, totalH); temp.frameCount = n; temp.comment = imageCur.comment; int[] dest = (int[])temp.pixels; int xx = 0; for (int i =0; i < n; i++) { Image img = (Image)frames.items[i]; int[] src = (int[])img.pixels; int w = img.width; for (int yy = 0; yy < totalH; yy++) Vm.arrayCopy(src, yy*w, dest, xx+yy*totalW, w); xx += w; } imageCur = temp; } catch (Exception e) { imageCur = (Image)frames.items[0]; // if an error occurs, we assume only the first frame } } public synchronized void imageComplete(int status) { switch (status) { default: case IMAGEERROR: case IMAGEABORTED: if (isGif && frames.size() > 0) joinImages(); else Vm.warning("ImageLoader: error"); isImageComplete = true; break; case STATICIMAGEDONE: case SINGLEFRAMEDONE: if (!isGif) // all other image types has a single frame isImageComplete = true; else { // since jdk can't correctly tell when the last frame of a multi-frame GIF // was reached, we have to keep loading until we repeat the first one. if (frames.size() > 0 && arraysEquals((int[])imageCur.pixels, (int[])((Image)frames.items[0]).pixels)) { joinImages(); isImageComplete = true; } else { frames.push(imageCur); imageCur = null; } } break; } if (isImageComplete) { producer.removeConsumer(this); notifyAll(); } } } /** Returns 0 */ public int getX() { return 0; } /** Returns 0 */ public int getY() { return 0; } /** Returns true if the given filename is a supported image: Png or Jpeg. Gif and Bmp are supported on JavaSE only. * @since TotalCross 1.0 */ public static boolean isSupported(String filename) { if (filename == null) return false; filename = filename.toLowerCase(); return filename.endsWith(".jpeg") || filename.endsWith(".jpg") || filename.endsWith(".png") || filename.endsWith(".gif") || filename.endsWith(".bmp"); } /** In a multi-frame image, returns a copy of the given frame. * In a single-frame image, gets a copy of the image. * @since TotalCross 1.12 */ final public Image getFrameInstance(int frame) throws ImageException // guich@tc112_7 { Image img = getCopy(width,height); int old = currentFrame; setCurrentFrame(frame); int[] from = (int[])this.pixels; int[] to = (int[])img.pixels; Vm.arrayCopy(from, 0, to, 0, from.length); setCurrentFrame(old); return img; } /** Applies the given color r,g,b values to all pixels of this image, * preserving the transparent color and alpha channel, if set. * @param color The color to be applied * @since TotalCross 1.12 */ final public void applyColor(int color) // guich@tc112_24 { int r2 = Color.getRed(color); int g2 = Color.getGreen(color); int b2 = Color.getBlue(color); double k = 128; int mr,mg,mb; mr = (int) (Math.sqrt((r2 + k) / k) * 0x10000); mg = (int) (Math.sqrt((g2 + k) / k) * 0x10000); mb = (int) (Math.sqrt((b2 + k) / k) * 0x10000); int[] pixels = (int[]) (frameCount == 1 ? this.pixels : this.pixelsOfAllFrames); for (int n = pixels.length; --n >= 0;) { int p = pixels[n]; if ((p & 0xFF000000) != 0) { int r = (mr * Color.getRed(p)) >> 16; int g = (mg * Color.getGreen(p)) >> 16; int b = (mb * Color.getBlue(p)) >> 16; if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255; pixels[n] = (p & 0xFF000000) | (r<<16) | (g<<8) | b; } } if (frameCount != 1) {currentFrame = 2; setCurrentFrame(0);} } /** Returns a smooth scaled instance of this image with a fixed aspect ratio * based on the given resolution (which is the resolution that you used to MAKE the image). The target size is computed as * <code>image_size*min(screen_size)/original_resolution</code> * @param originalRes The original resolution that the image was developed for. Its a good idea to create images for 320x320 and then scale them down. * @since TotalCross 1.12 */ final public Image smoothScaledFromResolution(int originalRes) throws ImageException // guich@tc112_23 { int k = Math.min(Settings.screenWidth,Settings.screenHeight); return getSmoothScaledInstance(width*k/originalRes, height*k/originalRes); } /** Returns true if the given Image object has the same size and RGB pixels of this one. * The alpha-channel is ignored. * @since TotalCross 1.3 */ public boolean equals(Object o) { if (o instanceof Image) { Image img = (Image)o; int w = this.frameCount > 1 ? this.widthOfAllFrames : this.width; int w2 = img.frameCount > 1 ? img.widthOfAllFrames : img.width; int h = this.height; int h2 = img.height; if (w != w2 || h != h2) return false; byte[] row1 = new byte[4*w]; byte[] row2 = new byte[4*w]; for (int y = 0; y < h; y++) { this.getPixelRow(row1, y); img .getPixelRow(row2, y); for (int k = row1.length; --k >= 0;) if (row1[k] != row2[k]) return false; } return true; } return false; } /** Applies the given color r,g,b values to all pixels of this image, * preserving the transparent color and alpha channel, if set. * This method is used to colorize the Android buttons. * * If the color's alpha is 0xAA, the image's alpha will also be changed. This is used by the Spinner. * * @param color The color to be applied * @since TotalCross 1.3 */ final public void applyColor2(int color) { int r2 = Color.getRed(color); int g2 = Color.getGreen(color); int b2 = Color.getBlue(color); boolean changeA = (color & 0xFF000000) == 0xAA000000; int m,p; int[] pixels = (int[]) (frameCount == 1 ? this.pixels : this.pixelsOfAllFrames); // the given color argument will be equivalent to the brighter color of this image. Here we search for that color int hi=0, hip=0; for (int n = pixels.length; --n >= 0;) if (((p = pixels[n]) & 0xFF000000) == 0xFF000000) // consider only opaque pixels { p &= 0x00FFFFFF; int r = (p >> 16) & 0xFF; int g = (p >> 8) & 0xFF; int b = (p ) & 0xFF; m = (r + g + b) / 3; if (m > hi) {hi = m; hip = p;} } int hiR = (hip >> 16) & 0xFF; int hiG = (hip >> 8) & 0xFF; int hiB = (hip ) & 0xFF; if (hiR == 0) hiR = 255; if (hiG == 0) hiG = 255; if (hiB == 0) hiB = 255; hi = hiR > hiG ? hiR : hiG; hi = hi > hiB ? hi : hiB; for (int n = pixels.length; --n >= 0;) { p = pixels[n]; if ((p & 0xFF000000) != 0) { int pr = (p >> 16) & 0xFF; int pg = (p >> 8) & 0xFF; int pb = p & 0xFF; int r = pr * r2 / hiR; int g = pg * g2 / hiG; int b = pb * b2 / hiB; if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255; if (changeA) { int a = pr > pg ? pr : pg; if (pb > a) a = pb; a = a * 255 / hi; if (a > 255) a = 255; pixels[n] = (a << 24) | (r<<16) | (g<<8) | b; } else pixels[n] = (p & 0xFF000000) | (r<<16) | (g<<8) | b; } } if (frameCount != 1) {currentFrame = 2; setCurrentFrame(0);} } ////////////////////// TOTALCROSS 2 //////////////////// /** @deprecated TotalCross 2 no longer uses the backColor parameter. */ public Image getSmoothScaledInstance(int newWidth, int newHeight, int backColor) throws ImageException // guich@350_22 { return getSmoothScaledInstance(newWidth, newHeight); } /** @deprecated TotalCross 2 no longer uses the backColor parameter. */ public Image smoothScaledBy(double scaleX, double scaleY, int backColor) throws ImageException // guich@402_6 { return smoothScaledBy(scaleX, scaleY); } /** @deprecated TotalCross 2 no longer uses the backColor parameter. */ public Image smoothScaledFixedAspectRatio(int newSize, boolean isHeight, int backColor) throws ImageException // guich@402_6 { return smoothScaledFixedAspectRatio(newSize, isHeight); } /** @deprecated TotalCross 2 no longer uses the backColor parameter. */ final public Image smoothScaledFromResolution(int originalRes, int backColor) throws ImageException // guich@tc112_23 { return smoothScaledFromResolution(originalRes); } /** Applies the given fade value to r,g,b of this image while preserving the alpha value. */ public void applyFade(int fadeValue) { int[] pixels = (int[])this.pixels; int lastColor = -1, lastFaded=0; for (int j = pixels.length; --j >= 0;) { int rgb = pixels[j]; if (rgb == lastColor) pixels[j] = lastFaded; else { lastColor = rgb; int a = ((rgb >> 24) & 0xFF); int r = ((rgb >> 16) & 0xFF) * fadeValue / 255; int g = ((rgb >> 8 ) & 0xFF) * fadeValue / 255; int b = (rgb & 0xFF) * fadeValue / 255; lastFaded = pixels[j] = (a << 24) | (r << 16) | (g << 8) | b; } } } /** Utility method used to change the frame count of an image. This method exists in * java only, not on device. The frame count of a TotalCross' Image is stored in a * comment inside the PNG file. If you create a PNG with many frames and don't want to * keep calling the setFrameCount manually, you can call this method like: * <pre> * Image.writeFrameCount("c:/project/src/images/people.png",2); * </pre> * Be careful that this must be done once only; this method does not exist in the device * and will abort the vm if you try to call it there! * @since TotalCross 3.1 */ public static void writeFrameCount(String filePath, int count) { try { Image img = new Image(filePath); if (img.getFrameCount() == count) throw new RuntimeException("The image "+filePath+" already has "+count+" frames! Please remove the code that called writeFrameCount!"); img.setFrameCount(count); File f = new File(filePath,File.CREATE_EMPTY); img.createPng(f); f.close(); Vm.debug("\n\nSuccess changing frame count of "+filePath+" to "+count+"! Now don't forget to comment or remove the code, and refresh your project so your IDE can reload the image.\n\n"); } catch (Exception e) { e.printStackTrace(); } } /** Gets a copy of this image; if the image is multi-framed, returns a copy of the first frame. * @since TotalCross 3.1 */ public Image getCopy() throws ImageException { return getFrameInstance(0); } }