package complexion.resource; import static complexion.common.Directions.EAST; import static complexion.common.Directions.NORTH; import static complexion.common.Directions.NORTHEAST; import static complexion.common.Directions.NORTHWEST; import static complexion.common.Directions.SOUTH; import static complexion.common.Directions.SOUTHEAST; import static complexion.common.Directions.SOUTHWEST; import static complexion.common.Directions.WEST; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.sixlegs.png.PngImage; import com.sixlegs.png.TextChunk; public class DMIParser { /** * Function for parsing .dmi metadata and building this.states * from it. * * @throws IOException */ void parse(InputStream source) throws IOException { // Load the PNG into memory and decode it PngImage load = new PngImage(); // Extract the image data itself from the PNG buffer = load.read(source, false); // do not close the input stream // Start with the tiling at the top left pixel_x = 0; pixel_y = 0; // Extract the text chunk containing the .dmi metadata TextChunk chunk = load.getTextChunk("Description"); if(chunk == null) throw new IOException("File has no metadata"); String metadata = chunk.getText(); // Parse the metadata List<SpriteInfoBlock> nodes = parseMetadata(metadata); // Interpret the syntax tree and build this.states // ----------------------------------------------- // information about a single state String state_name = ""; // For each frame, there should be a separate delay. List<Integer> state_delay = new ArrayList<Integer>(); int state_frames = 0; // Number of frames in this state int state_directions = 0; // Number of directions in this state boolean had_state = false; // Used to check whether a state was already created this.states = new HashMap<String,SpriteState>(); // Go over the syntax tree node by node and extract info // ===================================================== Iterator<SpriteInfoBlock> it = nodes.iterator(); while(it.hasNext()) { SpriteInfoBlock i = it.next(); // If there's data on the line, collect it // --------------------------------------- if(i.key.equals("dirs")) { // get the number of directions state_directions = Integer.parseInt(i.value); } else if(i.key.equals("frames")) { // number of frames per direction state_frames = Integer.parseInt(i.value); } else if(i.key.equals("delay")) { // Delay between animation frames, this is a list // in the form of "2,2,2,4", one for each frame for(String delay : i.value.split(",")) { state_delay.add(Integer.parseInt(delay)); } } else if(i.key.equals("version")){ if(!i.value.equals("4.0")){ throw new IOException(".DMI version "+i.value+" incompatible"); } } else if(i.key.equals("width")){ width = Integer.parseInt(i.value); } else if(i.key.equals("height")){ height = Integer.parseInt(i.value); } // See if we're done with this specific icon state. // We're done if another icon state follows, or if we're at the end of the file // ---------------------------------------------------------------------------- if(had_state) if(i.key.equals("state") || !it.hasNext()) { // Make sure this icon state is properly defined if(state_frames == 0 || state_directions == 0) { throw new IOException(".DMI metadata malformed"); } // If the state delays haven't been defined, go for a default if(state_delay.size() == 0) { while(state_delay.size() < state_frames) { state_delay.add(1); } } if(state_delay.size() != state_frames) { throw new IOException(".DMI metadata malformed"); } SpriteState state = generateSpriteState(state_frames, state_directions, state_delay); // intern state_name to make string comparison more efficient state_name = state_name.intern(); states.put(state_name, state); // add state to the sprite state_frames = 0; state_directions = 0; state_delay.clear(); } // A "state" line opens a new state if(i.key.equals("state")) { state_name = i.value.substring(1,i.value.length() - 1); had_state = true; } } } /** * Internal function for parsing the next N frames into a sprite state. * This will tile the buffer into sub-images and assign the sub-images * to state/frame/direction combinations. * * Modifies pixel_x/pixel_y values to "advance" the current tile. */ private SpriteState generateSpriteState(int frames, int directions, List<Integer> delays) throws IOException { SpriteState state = new SpriteState(); state.frames = new ArrayList<SpriteFrame>(); // Extract the width of the full image(not the tiles) int base_width = buffer.getWidth(); for(int framei = 0; framei < frames; framei++) { // Prepare a new frame to insert into our state SpriteFrame frame = new SpriteFrame(); frame.directions = new HashMap<Integer,BufferedImage>(); // This array simply maps tile indices to directions. // For example, the first tile in the frame would be the direction SOUTH. int dirs[] = {SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST}; if(directions != 1 && directions != 4 && directions != 8) { throw new IOException(".DMI metadata malformed"); } // Go over all directions and insert them into our frame for(int i=0; i < directions; i++) { // Find out which direction the next tile will be int next_dir = dirs[i]; // If we're at the end of the buffer on the right, go to the next line if(pixel_x >= base_width) { pixel_x = 0; pixel_y = pixel_y + height; } BufferedImage image = buffer.getSubimage(pixel_x, pixel_y, width, height); frame.directions.put(next_dir, image); pixel_x += width; // push to the next "row" } // Set the proper delay of the frame frame.delay = delays.get(framei); // Add the frame to our state state.frames.add(frame); } return state; } /** * Internal function for parsing plaintext metadata and building a kind of * abstract syntax tree from it. */ private List<SpriteInfoBlock> parseMetadata(String data) throws IOException { // Prepare a "syntax tree" to return List<SpriteInfoBlock> nodes = new ArrayList<SpriteInfoBlock>(); // Split the metadata into individual lines and parse them individually String[] lines = data.split("\n"); for(String line : lines) { // If the line is all spaces, or begins with #, ignore it if(line.matches("\\s*") || line.matches("#.*")) { continue; } // Create a regular expression to parse lines of the type: // key = value Pattern pattern = Pattern.compile("(\\s*)([^\\s]+)\\s*=\\s*(.*[^\\s])\\s*"); Matcher matcher = pattern.matcher(line); // Try to match our line against the key = value pattern boolean matchFound = matcher.matches(); if(!matchFound) throw new IOException("Malformed .DMI metadata"); // Extract key, value and indentation String indentation = matcher.group(1); String key = matcher.group(2); String value = matcher.group(3); // Build a new entry in our parsetree for it SpriteInfoBlock new_block = new SpriteInfoBlock(); new_block.indent = (indentation.length() != 0); new_block.key = key; new_block.value = value; nodes.add(new_block); } return nodes; } // Variables to store the parsing results in Map<String,SpriteState> states; int width, height = 0; private BufferedImage buffer; // Image we're currently taking apart private int pixel_x, pixel_y; // Current position on the image(0,0 is top-left corner) } /** * Private class used to temporarily store the key-value pairs in * that have been stored in the PNG metadata. */ class SpriteInfoBlock { boolean indent = false; // whether this key-value pair is indented String key = null; // the key(left-hand side) of the pair String value = null; // the value(right-hand side) of the pair }