/* * Copyright (c) 2009-2012 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.jme3.cursors.plugins; import com.jme3.asset.AssetInfo; import com.jme3.asset.AssetKey; import com.jme3.asset.AssetLoader; import com.jme3.util.BufferUtils; import com.jme3.util.LittleEndien; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.IntBuffer; import java.util.ArrayList; import javax.imageio.ImageIO; /** * Created Jun 5, 2012 9:45:58 AM * @author MadJack */ public class CursorLoader implements AssetLoader { final private static int FDE_OFFSET = 6; // first directory entry offset private boolean isIco; private boolean isAni; private boolean isCur; // .cur format if true /** * Loads and return a cursor file of one of the following format: .ani, .cur and .ico. * @param info The {@link AssetInfo} describing the cursor file. * @return A JmeCursor representation of the LWJGL's Cursor. * @throws IOException if the file is not found. */ public JmeCursor load(AssetInfo info) throws IOException { isIco = false; isAni = false; isCur = false; isIco = ((AssetKey) info.getKey()).getExtension().equals("ico"); if (!isIco) { isCur = ((AssetKey) info.getKey()).getExtension().equals("cur"); if (!isCur) { isAni = ((AssetKey) info.getKey()).getExtension().equals("ani"); } } if (!isAni && !isIco && !isCur) { throw new IllegalArgumentException("Cursors supported are .ico, .cur or .ani"); } InputStream in = null; try { in = info.openStream(); return loadCursor(in); } finally { if (in != null) { in.close(); } } } private JmeCursor loadCursor(InputStream inStream) throws IOException { byte[] icoimages = new byte[0]; // new byte [0] facilitates read() if (isAni) { CursorLoader.CursorImageData ciDat = new CursorLoader.CursorImageData(); int numIcons = 0; int jiffy = 0; // not using those but keeping references for now. int steps = 0; int width = 0; int height = 0; int flag = 0; // we don't use that. int[] rate = null; int[] animSeq = null; ArrayList<byte[]> icons; DataInput leIn = new LittleEndien(inStream); int riff = leIn.readInt(); if (riff == 0x46464952) { // RIFF // read next int (file length), discarding it, we don't need that. leIn.readInt(); int nextInt = 0; nextInt = getNext(leIn); if (nextInt == 0x4e4f4341) { // We have ACON, we do nothing // System.out.println("We have ACON. Next!"); nextInt = getNext(leIn); while (nextInt >= 0) { if (nextInt == 0x68696e61) { // System.out.println("we have 'anih' header"); leIn.skipBytes(8); // internal struct length (always 36) numIcons = leIn.readInt(); steps = leIn.readInt(); // number of blits for ani cycles width = leIn.readInt(); height = leIn.readInt(); leIn.skipBytes(8); jiffy = leIn.readInt(); flag = leIn.readInt(); nextInt = leIn.readInt(); } else if (nextInt == 0x65746172) { // found a 'rate' of animation // System.out.println("we have 'rate'."); // Fill rate here. // Rate is synchronous with frames. int length = leIn.readInt(); rate = new int[length / 4]; for (int i = 0; i < length / 4; i++) { rate[i] = leIn.readInt(); } nextInt = leIn.readInt(); } else if (nextInt == 0x20716573) { // found a 'seq ' of animation // System.out.println("we have 'seq '."); // Fill animation sequence here int length = leIn.readInt(); animSeq = new int[length / 4]; for (int i = 0; i < length / 4; i++) { animSeq[i] = leIn.readInt(); } nextInt = leIn.readInt(); } else if (nextInt == 0x5453494c) { // Found a LIST // System.out.println("we have 'LIST'."); int length = leIn.readInt(); nextInt = leIn.readInt(); if (nextInt == 0x4f464e49) { // Got an INFO, skip its length // this part consist of Author, title, etc leIn.skipBytes(length - 4); // System.out.println(" Discarding INFO (skipped = " + skipped + ")"); nextInt = leIn.readInt(); } else if (nextInt == 0x6d617266) { // found a 'fram' for animation // System.out.println("we have 'fram'."); if (leIn.readInt() == 0x6e6f6369) { // we have 'icon' // We have an icon and from this point on // the rest is only icons. int icoLength = leIn.readInt(); ciDat.numImages = numIcons; icons = new ArrayList<byte[]>(numIcons); for (int i = 0; i < numIcons; i++) { if (i > 0) { // skip 'icon' header and length as they are // known already and won't change. leIn.skipBytes(8); } byte[] data = new byte[icoLength]; ((InputStream) leIn).read(data, 0, icoLength); // in case the header didn't have width or height // get it from first image. if (width == 0 || height == 0 && i == 1) { width = data[6]; height = data[7]; } icons.add(data); } // at this point we have the icons, rates (either // through jiffy or rate array, the sequence (if // applicable) and the ani header info. // Put things together. ciDat.assembleCursor(icons, rate, animSeq, jiffy, steps, width, height); ciDat.completeCursor(); nextInt = leIn.readInt(); // if for some reason there's JUNK (nextInt > -1) // bail out. nextInt = nextInt > -1 ? -1 : nextInt; } } } } } return setJmeCursor(ciDat); } else if (riff == 0x58464952) { throw new IllegalArgumentException("Big-Endian RIFX is not supported. Sorry."); } else { throw new IllegalArgumentException("Unknown format."); } } else if (isCur || isIco) { DataInputStream in = new DataInputStream(inStream); ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buffer = new byte[16384]; int bytesRead; while ((bytesRead = in.read(buffer)) >= 0) { out.write(buffer, 0, bytesRead); } icoimages = out.toByteArray(); } BufferedImage bi[] = parseICOImage(icoimages); int hotSpotX = 0; int hotSpotY = 0; CursorLoader.CursorImageData cid = new CursorLoader.CursorImageData(bi, 0, hotSpotX, hotSpotY, 0); if (isCur) { /* * Per http://msdn.microsoft.com/en-us/library/ms997538.aspx * every .cur file should provide hotspot coordinates. */ hotSpotX = icoimages[FDE_OFFSET + 4] + icoimages[FDE_OFFSET + 5] * 255; hotSpotY = icoimages[FDE_OFFSET + 6] + icoimages[FDE_OFFSET + 7] * 255; cid.xHotSpot = hotSpotX; /* * Flip the Y-coordinate. */ cid.yHotSpot = cid.height - 1 - hotSpotY; } cid.completeCursor(); return setJmeCursor(cid); } private JmeCursor setJmeCursor(CursorLoader.CursorImageData cid) { JmeCursor jmeCursor = new JmeCursor(); // set cursor's params. jmeCursor.setWidth(cid.width); jmeCursor.setHeight(cid.height); jmeCursor.setxHotSpot(cid.xHotSpot); jmeCursor.setyHotSpot(cid.yHotSpot); jmeCursor.setNumImages(cid.numImages); jmeCursor.setImagesDelay(cid.imgDelay); jmeCursor.setImagesData(cid.data); // System.out.println("Width = " + cid.width); // System.out.println("Height = " + cid.height); // System.out.println("HSx = " + cid.xHotSpot); // System.out.println("HSy = " + cid.yHotSpot); // System.out.println("# img = " + cid.numImages); return jmeCursor; } private BufferedImage[] parseICOImage(byte[] icoimage) throws IOException { /* * Most of this is original code by Jeff Friesen at * http://www.informit.com/articles/article.aspx?p=1186882&seqNum=3 */ BufferedImage[] bi; // Check resource type field. int DE_LENGTH = 16; // directory entry length int BMIH_LENGTH = 40; // BITMAPINFOHEADER length if (icoimage[2] != 1 && icoimage[2] != 2 || icoimage[3] != 0) { throw new IllegalArgumentException("Bad data in ICO/CUR file. ImageType has to be either 1 or 2."); } int numImages = ubyte(icoimage[5]); numImages <<= 8; numImages |= icoimage[4]; bi = new BufferedImage[numImages]; int[] colorCount = new int[numImages]; for (int i = 0; i < numImages; i++) { int width = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH]); int height = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 1]); colorCount[i] = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 2]); int bytesInRes = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 11]); bytesInRes <<= 8; bytesInRes |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 10]); bytesInRes <<= 8; bytesInRes |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 9]); bytesInRes <<= 8; bytesInRes |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 8]); int imageOffset = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 15]); imageOffset <<= 8; imageOffset |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 14]); imageOffset <<= 8; imageOffset |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 13]); imageOffset <<= 8; imageOffset |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 12]); if (icoimage[imageOffset] == 40 && icoimage[imageOffset + 1] == 0 && icoimage[imageOffset + 2] == 0 && icoimage[imageOffset + 3] == 0) { // BITMAPINFOHEADER detected int _width = ubyte(icoimage[imageOffset + 7]); _width <<= 8; _width |= ubyte(icoimage[imageOffset + 6]); _width <<= 8; _width |= ubyte(icoimage[imageOffset + 5]); _width <<= 8; _width |= ubyte(icoimage[imageOffset + 4]); // If width is 0 (for 256 pixels or higher), _width contains // actual width. if (width == 0) { width = _width; } int _height = ubyte(icoimage[imageOffset + 11]); _height <<= 8; _height |= ubyte(icoimage[imageOffset + 10]); _height <<= 8; _height |= ubyte(icoimage[imageOffset + 9]); _height <<= 8; _height |= ubyte(icoimage[imageOffset + 8]); // If height is 0 (for 256 pixels or higher), _height contains // actual height times 2. if (height == 0) { height = _height >> 1; // Divide by 2. } int planes = ubyte(icoimage[imageOffset + 13]); planes <<= 8; planes |= ubyte(icoimage[imageOffset + 12]); int bitCount = ubyte(icoimage[imageOffset + 15]); bitCount <<= 8; bitCount |= ubyte(icoimage[imageOffset + 14]); // If colorCount [i] is 0, the number of colors is determined // from the planes and bitCount values. For example, the number // of colors is 256 when planes is 1 and bitCount is 8. Leave // colorCount [i] set to 0 when planes is 1 and bitCount is 32. if (colorCount[i] == 0) { if (planes == 1) { if (bitCount == 1) { colorCount[i] = 2; } else if (bitCount == 4) { colorCount[i] = 16; } else if (bitCount == 8) { colorCount[i] = 256; } else if (bitCount != 32) { colorCount[i] = (int) Math.pow(2, bitCount); } } else { colorCount[i] = (int) Math.pow(2, bitCount * planes); } } bi[i] = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // Parse image to image buffer. int colorTableOffset = imageOffset + BMIH_LENGTH; if (colorCount[i] == 2) { int xorImageOffset = colorTableOffset + 2 * 4; int scanlineBytes = calcScanlineBytes(width, 1); int andImageOffset = xorImageOffset + scanlineBytes * height; int[] masks = {128, 64, 32, 16, 8, 4, 2, 1}; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { int index; if ((ubyte(icoimage[xorImageOffset + row * scanlineBytes + col / 8]) & masks[col % 8]) != 0) { index = 1; } else { index = 0; } int rgb = 0; rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + 2])); rgb <<= 8; rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + 1])); rgb <<= 8; rgb |= (ubyte(icoimage[colorTableOffset + index * 4])); if ((ubyte(icoimage[andImageOffset + row * scanlineBytes + col / 8]) & masks[col % 8]) != 0) { bi[i].setRGB(col, height - 1 - row, rgb); } else { bi[i].setRGB(col, height - 1 - row, 0xff000000 | rgb); } } } } else if (colorCount[i] == 16) { int xorImageOffset = colorTableOffset + 16 * 4; int scanlineBytes = calcScanlineBytes(width, 4); int andImageOffset = xorImageOffset + scanlineBytes * height; int[] masks = {128, 64, 32, 16, 8, 4, 2, 1}; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { int index; if ((col & 1) == 0) // even { index = ubyte(icoimage[xorImageOffset + row * scanlineBytes + col / 2]); index >>= 4; } else { index = ubyte(icoimage[xorImageOffset + row * scanlineBytes + col / 2]) & 15; } int rgb = 0; rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + 2])); rgb <<= 8; rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + 1])); rgb <<= 8; rgb |= (ubyte(icoimage[colorTableOffset + index * 4])); if ((ubyte(icoimage[andImageOffset + row * calcScanlineBytes(width, 1) + col / 8]) & masks[col % 8]) != 0) { bi[i].setRGB(col, height - 1 - row, rgb); } else { bi[i].setRGB(col, height - 1 - row, 0xff000000 | rgb); } } } } else if (colorCount[i] == 256) { int xorImageOffset = colorTableOffset + 256 * 4; int scanlineBytes = calcScanlineBytes(width, 8); int andImageOffset = xorImageOffset + scanlineBytes * height; int[] masks = {128, 64, 32, 16, 8, 4, 2, 1}; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { int index; index = ubyte(icoimage[xorImageOffset + row * scanlineBytes + col]); int rgb = 0; rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + 2])); rgb <<= 8; rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + 1])); rgb <<= 8; rgb |= (ubyte(icoimage[colorTableOffset + index * 4])); if ((ubyte(icoimage[andImageOffset + row * calcScanlineBytes(width, 1) + col / 8]) & masks[col % 8]) != 0) { bi[i].setRGB(col, height - 1 - row, rgb); } else { bi[i].setRGB(col, height - 1 - row, 0xff000000 | rgb); } } } } else if (colorCount[i] == 0) { int scanlineBytes = calcScanlineBytes(width, 32); for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { int rgb = ubyte(icoimage[colorTableOffset + row * scanlineBytes + col * 4 + 3]); rgb <<= 8; rgb |= ubyte(icoimage[colorTableOffset + row * scanlineBytes + col * 4 + 2]); rgb <<= 8; rgb |= ubyte(icoimage[colorTableOffset + row * scanlineBytes + col * 4 + 1]); rgb <<= 8; rgb |= ubyte(icoimage[colorTableOffset + row * scanlineBytes + col * 4]); bi[i].setRGB(col, height - 1 - row, rgb); } } } } else if (ubyte(icoimage[imageOffset]) == 0x89 && icoimage[imageOffset + 1] == 0x50 && icoimage[imageOffset + 2] == 0x4e && icoimage[imageOffset + 3] == 0x47 && icoimage[imageOffset + 4] == 0x0d && icoimage[imageOffset + 5] == 0x0a && icoimage[imageOffset + 6] == 0x1a && icoimage[imageOffset + 7] == 0x0a) { // PNG detected ByteArrayInputStream bais; bais = new ByteArrayInputStream(icoimage, imageOffset, bytesInRes); bi[i] = ImageIO.read(bais); } else { throw new IllegalArgumentException("Bad data in ICO/CUR file. BITMAPINFOHEADER or PNG " + "expected"); } } icoimage = null; // This array can now be garbage collected. return bi; } private int ubyte(byte b) { return (b < 0) ? 256 + b : b; // Convert byte to unsigned byte. } private int calcScanlineBytes(int width, int bitCount) { // Calculate minimum number of double-words required to store width // pixels where each pixel occupies bitCount bits. XOR and AND bitmaps // are stored such that each scanline is aligned on a double-word // boundary. return (((width * bitCount) + 31) / 32) * 4; } private int getNext(DataInput in) throws IOException { return in.readInt(); } private class CursorImageData { int width; int height; int xHotSpot; int yHotSpot; int numImages; IntBuffer imgDelay; IntBuffer data; public CursorImageData() { } CursorImageData(BufferedImage[] bi, int delay, int hsX, int hsY, int curType) { // cursor type // 0 - Undefined (an array of images inside an ICO) // 1 - ICO // 2 - CUR IntBuffer singleCursor = null; ArrayList<IntBuffer> cursors = new ArrayList<IntBuffer>(); int bwidth = 0; int bheight = 0; boolean multIcons = false; // make the cursor image for (int i = 0; i < bi.length; i++) { BufferedImage img = bi[i]; bwidth = img.getWidth(); bheight = img.getHeight(); if (curType == 1) { hsX = 0; hsY = bheight - 1; } else if (curType == 2) { if (hsY == 0) { // make sure we flip if 0 hsY = bheight - 1; } } else { // We force to choose 32x32 icon from // the array of icons in that ICO file. if (bwidth != 32 && bheight != 32) { multIcons = true; continue; } else { if (img.getType() != 2) { continue; } else { // force hotspot hsY = bheight - 1; } } } // We flip our image because .ICO and .CUR will always be reversed. AffineTransform trans = AffineTransform.getScaleInstance(1, -1); trans.translate(0, -img.getHeight(null)); AffineTransformOp op = new AffineTransformOp(trans, AffineTransformOp.TYPE_BILINEAR); img = op.filter(img, null); singleCursor = BufferUtils.createIntBuffer(img.getWidth() * img.getHeight()); DataBufferInt dataIntBuf = (DataBufferInt) img.getData().getDataBuffer(); singleCursor = IntBuffer.wrap(dataIntBuf.getData()); cursors.add(singleCursor); } int count; if (multIcons) { bwidth = 32; bheight = 32; count = 1; } else { count = cursors.size(); } // put the image in the IntBuffer data = BufferUtils.createIntBuffer(bwidth * bheight); imgDelay = BufferUtils.createIntBuffer(bi.length); for (int i = 0; i < count; i++) { data.put(cursors.get(i)); if (delay > 0) { imgDelay.put(delay); } } width = bwidth; height = bheight; xHotSpot = hsX; yHotSpot = hsY; numImages = count; data.rewind(); if (imgDelay != null) { imgDelay.rewind(); } } private void addFrame(byte[] imgData, int rate, int jiffy, int width, int height, int numSeq) throws IOException { BufferedImage bi[] = parseICOImage(imgData); int hotspotx = 0; int hotspoty = 0; int type = imgData[2] | imgData[3]; if (type == 2) { // CUR type, hotspot might be stored. hotspotx = imgData[10] | imgData[11]; hotspoty = imgData[12] | imgData[13]; } else if (type == 1) { // ICO type, hotspot not stored. Put at 0, height - 1 // because it's flipped. hotspotx = 0; hotspoty = height - 1; } // System.out.println("Image type = " + (type == 1 ? "CUR" : "ICO")); if (rate == 0) { rate = jiffy; } CursorLoader.CursorImageData cid = new CursorLoader.CursorImageData(bi, rate, hotspotx, hotspoty, type); if (width == 0) { this.width = cid.width; } else { this.width = width; } if (height == 0) { this.height = cid.height; } else { this.height = height; } if (data == null) { if (numSeq > numImages) { data = BufferUtils.createIntBuffer(this.width * this.height * numSeq); } else { data = BufferUtils.createIntBuffer(this.width * this.height * numImages); } data.put(cid.data); } else { data.put(cid.data); } if (imgDelay == null && (numImages > 1 || numSeq > 1)) { if (numSeq > numImages) { imgDelay = BufferUtils.createIntBuffer(numSeq); } else { imgDelay = BufferUtils.createIntBuffer(numImages); } imgDelay.put(cid.imgDelay); } else if (imgData != null) { imgDelay.put(cid.imgDelay); } xHotSpot = cid.xHotSpot; yHotSpot = cid.yHotSpot; cid = null; } void assembleCursor(ArrayList<byte[]> icons, int[] rate, int[] animSeq, int jiffy, int steps, int width, int height) throws IOException { // Jiffy multiplicator for LWJGL's delay, which is in milisecond. final int MULT = 17; numImages = icons.size(); int frRate = 0; byte[] frame = new byte[0]; // if we have an animation sequence we use that // since the sequence can be larger than the number // of images in the ani if it reuses one or more of those // images. if (animSeq != null && animSeq.length > 0) { for (int i = 0; i < animSeq.length; i++) { if (rate != null) { frRate = rate[i] * MULT; } else { frRate = jiffy * MULT; } // the frame # is the one in the animation sequence frame = icons.get(animSeq[i]); addFrame(frame, frRate, jiffy, width, height, animSeq.length); // System.out.println("delay of " + frRate); } } else { for (int i = 0; i < icons.size(); i++) { frame = icons.get(i); if (rate == null) { frRate = jiffy * MULT; } else { frRate = rate[i] * MULT; } addFrame(frame, frRate, jiffy, width, height, 0); // System.out.println("delay of " + frRate); } } } /** * Called to rewind the buffers after filling them. */ void completeCursor() { if (numImages == 1) { imgDelay = null; } else { imgDelay.rewind(); } data.rewind(); } } }