/* * Copyright (C) 2006, 2007 Clam <clamisgood@gmail.com> * Copyright (C) 2008, 2009 Quadduc <quadduc@gmail.com> * Copyright (C) 2011 IsmAvatar <IsmAvatar@gmail.com> * Copyright (C) 2013 Robert B. Colton * * This file is part of LateralGM. * LateralGM is free software and comes with ABSOLUTELY NO WARRANTY. * See LICENSE for details. */ package org.lateralgm.resources; import java.awt.Color; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferDouble; import java.awt.image.DataBufferFloat; import java.awt.image.DataBufferInt; import java.awt.image.DataBufferShort; import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; import javax.imageio.ImageIO; import org.lateralgm.file.ProjectFile; import org.lateralgm.main.LGM; import org.lateralgm.main.Util; import org.lateralgm.messages.Messages; import org.lateralgm.util.PropertyMap; import org.lateralgm.util.PropertyMap.PropertyUpdateEvent; import org.lateralgm.util.PropertyMap.PropertyUpdateListener; public class Sprite extends InstantiableResource<Sprite,Sprite.PSprite> implements Resource.Viewable { public enum BBMode { AUTO,FULL,MANUAL } public enum Effects { INVERT,FLIP,ROTATE } public enum MaskShape { PRECISE,RECTANGLE,DISK,DIAMOND,POLYGON } public final ImageList subImages = new ImageList(); public enum PSprite { TRANSPARENT,SHAPE,ALPHA_TOLERANCE,SEPARATE_MASK,SMOOTH_EDGES,PRELOAD,ORIGIN_X,ORIGIN_Y,BB_MODE, BB_LEFT,BB_RIGHT,BB_TOP,BB_BOTTOM,TILE_HORIZONTALLY,TILE_VERTICALLY,FOR3D } private static final EnumMap<PSprite,Object> DEFS = PropertyMap.makeDefaultMap(PSprite.class, false,MaskShape.RECTANGLE,0,false,false,true,0,0,BBMode.AUTO,0,31,0,31,false,false,false); private SoftReference<BufferedImage> imageCache = null; private final SpritePropertyListener spl = new SpritePropertyListener(); public Sprite() { this(null); } public Sprite(ResourceReference<Sprite> r) { super(r); properties.getUpdateSource(PSprite.TRANSPARENT).addListener(spl); properties.getUpdateSource(PSprite.ALPHA_TOLERANCE).addListener(spl); properties.getUpdateSource(PSprite.BB_MODE).addListener(spl); } public Sprite makeInstance(ResourceReference<Sprite> r) { return new Sprite(r); } public BufferedImage addSubImage() { int w = subImages.getWidth(); int h = subImages.getHeight(); if (w == 0 || h == 0) { w = 32; h = 32; } BufferedImage sub = new BufferedImage(w,h,BufferedImage.TYPE_3BYTE_BGR); Graphics g = sub.getGraphics(); g.setColor(Color.WHITE); g.fillRect(0,0,w,h); return sub; } public BufferedImage addSubImage(byte[] imagedata) { BufferedImage result = null; try { ByteArrayInputStream imagestr = new ByteArrayInputStream(imagedata); result = ImageIO.read(imagestr); subImages.add(result); } catch (IOException ex) { System.err.println(Messages.format("Sprite.ERROR_SUBIMAGE",subImages.size(),getId())); //$NON-NLS-1$ } return result; } private void updateBoundingBox() { BBMode mode = get(PSprite.BB_MODE); if (mode == null) { return; } switch (mode) { case AUTO: Rectangle r = getOverallBounds(subImages,(Boolean) get(PSprite.TRANSPARENT), (int) get(PSprite.ALPHA_TOLERANCE)); put(PSprite.BB_LEFT,r.x); put(PSprite.BB_RIGHT,r.x + r.width); put(PSprite.BB_TOP,r.y); put(PSprite.BB_BOTTOM,r.y + r.height); break; case FULL: put(PSprite.BB_LEFT,0); put(PSprite.BB_RIGHT,subImages.getWidth() - 1); put(PSprite.BB_TOP,0); put(PSprite.BB_BOTTOM,subImages.getHeight() - 1); break; default: break; } } public int getWidth() { return subImages.getWidth(); } public int getHeight() { return subImages.getHeight(); } public static Rectangle getOverallBounds(ImageList l, boolean transPixel, int tolerance) { Rectangle r = new Rectangle(); for (BufferedImage bi : l) getCropBounds(bi,r,transPixel,tolerance); if (r.width > 0 && r.height > 0) { r.width--; r.height--; } return r; } public static void getCropBounds(BufferedImage img, Rectangle u, boolean transPixel, int tolerance) { if (transPixel) getCropBoundsPixel(img,u); else getCropBoundsAlpha(img,u,tolerance); } public static void getCropBoundsAlpha(BufferedImage img, Rectangle u, int tolerance) { int width = img.getWidth(); int height = img.getHeight(); boolean unz = u.width > 0 && u.height > 0; int uy2 = unz ? u.y + u.height - 1 : -1; int y2 = height - 1; y2loop: for (; y2 > uy2; y2--) for (int i = 0; i < width; i++) if (((img.getRGB(i,y2) >> 24) & 0xff) > tolerance) break y2loop; int ux2 = unz ? u.x + u.width - 1 : -1; int x2 = width - 1; x2loop: for (; x2 > ux2; x2--) for (int j = 0; j <= y2; j++) if (((img.getRGB(x2,j) >> 24) & 0xff) > tolerance) break x2loop; int uy1 = unz ? u.y : y2; int y1 = 0; y1loop: for (; y1 < uy1; y1++) for (int i = 0; i <= x2; i++) if (((img.getRGB(i,y1) >> 24) & 0xff) > tolerance) break y1loop; int ux1 = unz ? u.x : x2; int x1 = 0; x1loop: for (; x1 < ux1; x1++) for (int j = y1; j <= y2; j++) if (((img.getRGB(x1,j) >> 24) & 0xff) > tolerance) break x1loop; u.x = x1; u.y = y1; u.width = 1 + x2 - x1; u.height = 1 + y2 - y1; } public static void getCropBoundsPixel(BufferedImage img, Rectangle u) { int transparent = img.getRGB(0,img.getHeight() - 1); int width = img.getWidth(); int height = img.getHeight(); boolean unz = u.width > 0 && u.height > 0; int uy2 = unz ? u.y + u.height - 1 : -1; int y2 = height - 1; y2loop: for (; y2 > uy2; y2--) for (int i = 0; i < width; i++) if (img.getRGB(i,y2) != transparent) break y2loop; int ux2 = unz ? u.x + u.width - 1 : -1; int x2 = width - 1; x2loop: for (; x2 > ux2; x2--) for (int j = 0; j <= y2; j++) if (img.getRGB(x2,j) != transparent) break x2loop; int uy1 = unz ? u.y : y2; int y1 = 0; y1loop: for (; y1 < uy1; y1++) for (int i = 0; i <= x2; i++) if (img.getRGB(i,y1) != transparent) break y1loop; int ux1 = unz ? u.x : x2; int x1 = 0; x1loop: for (; x1 < ux1; x1++) for (int j = y1; j <= y2; j++) if (img.getRGB(x1,j) != transparent) break x1loop; u.x = x1; u.y = y1; u.width = 1 + x2 - x1; u.height = 1 + y2 - y1; } public BufferedImage getDisplayImage() { BufferedImage bi; if (imageCache != null) { bi = imageCache.get(); if (bi != null) return bi; } if (subImages.size() < 1) return null; bi = subImages.get(0); if (get(PSprite.TRANSPARENT)) bi = Util.getTransparentImage(bi); imageCache = new SoftReference<BufferedImage>(bi); return bi; } @Override protected void postCopy(Sprite dest) { super.postCopy(dest); for (int j = 0; j < subImages.size(); j++) dest.subImages.add(Util.cloneImage(subImages.get(j))); } @Override protected void fireUpdate() { if (imageCache != null) imageCache.clear(); updateBoundingBox(); super.fireUpdate(); } public final class ImageList extends ArrayList<BufferedImage> { private static final long serialVersionUID = 1L; private ImageList() { } /** Returns the byte length of a DataBuffer **/ //TODO: This function reports astronomical values for some reason. public long getDataBytes(DataBuffer buffer) { int dataType = buffer.getDataType(); long length = 0; short bytes = 0; switch (dataType) { case DataBuffer.TYPE_BYTE: length = ((DataBufferByte) buffer).getData().length; bytes = 1; break; case DataBuffer.TYPE_USHORT: length = ((DataBufferShort) buffer).getData().length; bytes = 2; break; case DataBuffer.TYPE_INT: length = ((DataBufferInt) buffer).getData().length; bytes = 4; break; case DataBuffer.TYPE_FLOAT: length = ((DataBufferFloat) buffer).getData().length; bytes = 4; break; case DataBuffer.TYPE_DOUBLE: length = ((DataBufferDouble) buffer).getData().length; bytes = 8; break; default: throw new IllegalArgumentException("Unknown data buffer type: " + dataType); } //JOptionPane.showMessageDialog(null,bytes); return length * bytes; } /** Returns the size of the image list in bytes */ public long getSize() { long count = 0; for (int i = 0; i < this.size(); i++) { count += this.getSize(i);//getDataBytes(this.get(i).getData().getDataBuffer()); } return count; } /** Returns the size of the subimage in bytes */ public long getSize(int index) { return this.get(index).getWidth() * this.get(index).getHeight() * 4; //if (this.size() > index) { //return getDataBytes(this.get(index).getRaster().getDataBuffer()); //} //return 0; } public int getWidth() { if (size() > 0) return get(0).getWidth(); return 0; } public int getHeight() { if (size() > 0) return get(0).getHeight(); return 0; } @Override public boolean add(BufferedImage e) { super.add(e); fireUpdate(); return true; } @Override public void add(int index, BufferedImage element) { super.add(index,element); fireUpdate(); } @Override public boolean addAll(Collection<? extends BufferedImage> c) { boolean u = super.addAll(c); if (u) fireUpdate(); return u; } @Override public boolean addAll(int index, Collection<? extends BufferedImage> c) { boolean u = super.addAll(index,c); if (u) fireUpdate(); return u; } public boolean replace(BufferedImage obi, BufferedImage nbi) { int i = indexOf(obi); if (i < 0) return false; set(i,nbi); return true; } @Override public void clear() { super.clear(); fireUpdate(); } @Override public BufferedImage remove(int index) { BufferedImage i = super.remove(index); fireUpdate(); return i; } @Override public boolean remove(Object o) { boolean u = super.remove(o); if (u) fireUpdate(); return u; } @Override public boolean removeAll(Collection<?> c) { boolean u = super.removeAll(c); if (u) fireUpdate(); return u; } @Override protected void removeRange(int fromIndex, int toIndex) { super.removeRange(fromIndex,toIndex); fireUpdate(); } @Override public boolean retainAll(Collection<?> c) { boolean u = super.retainAll(c); if (u) fireUpdate(); return u; } @Override public BufferedImage set(int index, BufferedImage element) { BufferedImage i = super.set(index,element); fireUpdate(); return i; } } @Override protected PropertyMap<PSprite> makePropertyMap() { if (LGM.currentFile.format != null && LGM.currentFile.format.getOwner() == ProjectFile.FormatFlavor.GM_OWNER) DEFS.put(PSprite.TRANSPARENT,LGM.currentFile.format.getVersion() <= 600); return new PropertyMap<PSprite>(PSprite.class,this,DEFS); } private class SpritePropertyListener extends PropertyUpdateListener<PSprite> { @Override public void updated(PropertyUpdateEvent<PSprite> e) { switch (e.key) { case TRANSPARENT: fireUpdate(); break; case BB_MODE: case ALPHA_TOLERANCE: updateBoundingBox(); break; default: //TODO: maybe put a failsafe here? break; } } } }