Skip to content

Commit

Permalink
Merge pull request #20 from DD2480-Group-11/issue/12
Browse files Browse the repository at this point in the history
Implemented import functionality for Aseprite animations
  • Loading branch information
danhalv authored Mar 9, 2021
2 parents 5867e48 + e6512f6 commit 2163073
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 12 deletions.
174 changes: 166 additions & 8 deletions src/de/gurkenlabs/litiengine/graphics/animation/AsepriteHandler.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,181 @@
package de.gurkenlabs.litiengine.graphics.animation;

import java.io.File;
import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import javax.imageio.ImageIO;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonObject;

import java.util.HashMap;
import java.util.Map;
import java.util.List;

import de.gurkenlabs.litiengine.graphics.Spritesheet;
import de.gurkenlabs.litiengine.graphics.animation.Animation;
import de.gurkenlabs.litiengine.graphics.animation.KeyFrame;
import de.gurkenlabs.litiengine.graphics.Spritesheet;

/**
* Offers an interface to import Aseprite JSON export format.
* Note: requires animation key frames to have same dimensions to support internal animation format.
* */
public class AsepriteHandler {

/**
* Thrown to indicate error when importing Aseprite JSON format.
* */
public static class ImportAnimationException extends Error {
public ImportAnimationException(String message) {
super(message);
}
}

/**
* Imports an Aseprite animation (.json + sprite sheet).
* Note: searches for sprite sheet path through .json metadata, specifically 'image' element. This should be an absolute path in system.
*
* @param jsonPath path (including filename) to Aseprite JSON.
*
* @return Animation object represented by each key frame in Aseprite sprite sheet.
* */
public static Animation importAnimation(String jsonPath) throws IOException, FileNotFoundException, AsepriteHandler.ImportAnimationException {

JsonElement rootElement = null;
try { rootElement = getRootJsonElement(jsonPath); }
catch(FileNotFoundException e) {
throw new FileNotFoundException("FileNotFoundException: Could not find .json file " + jsonPath);
}

String spriteSheetPath = getSpriteSheetPath(rootElement);
File spriteSheetFile = new File(spriteSheetPath);
if(!spriteSheetFile.exists()) {
throw new FileNotFoundException("FileNotFoundException: Could not find sprite sheet file. " +
"Expected location is 'image' in .json metadata, which evaluates to: " + spriteSheetPath);
}

Dimension keyFrameDimensions = getKeyFrameDimensions(rootElement);
if(areKeyFramesSameDimensions(rootElement, keyFrameDimensions)) {

BufferedImage image = null;
try { image = ImageIO.read(spriteSheetFile); }
catch(IOException e) {
throw new IOException("IOException: Could not write sprite sheet data to BufferedImage object.");
}

Spritesheet spriteSheet = new Spritesheet(image,
spriteSheetPath,
(int)keyFrameDimensions.getWidth(),
(int)keyFrameDimensions.getHeight());

return new Animation(spriteSheet, false, getKeyFrameDurations(rootElement));
}

throw new AsepriteHandler.ImportAnimationException("AsepriteHandler.ImportAnimationException: animation key frames require same dimensions.");
}

/**
* @param jsonPath path (including filename) to Aseprite .json file.
*
* @return root element of JSON data.
* */
private static JsonElement getRootJsonElement(String jsonPath) throws FileNotFoundException {

File jsonFile = new File(jsonPath);

try {
JsonElement rootElement = JsonParser.parseReader(new FileReader(jsonFile));
return rootElement;
}
catch(FileNotFoundException e) { throw e; }
}

/**
* @param rootElement root element of JSON data.
*
* @return path (including filename) to animation sprite sheet.
* */
private static String getSpriteSheetPath(JsonElement rootElement) {

JsonElement metaData = rootElement.getAsJsonObject().get("meta");
String spriteSheetPath = metaData.getAsJsonObject().get("image").getAsString();

return spriteSheetPath;
}

/**
* @param rootElement root element of JSON data.
*
* @return dimensions of first key frame.
* */
private static Dimension getKeyFrameDimensions(JsonElement rootElement) {

JsonElement frames = rootElement.getAsJsonObject().get("frames");

JsonObject firstFrameObject = frames.getAsJsonObject().entrySet().iterator().next().getValue().getAsJsonObject();
JsonObject frameDimensions = firstFrameObject.get("sourceSize").getAsJsonObject();

int frameWidth = frameDimensions.get("w").getAsInt();
int frameHeight = frameDimensions.get("h").getAsInt();

return new Dimension(frameWidth, frameHeight);
}

/**
* @param rootElement root element of JSON data.
* @param expected expected dimensions of each key frame.
*
* @return true if key frames have same duration.
* */
private static boolean areKeyFramesSameDimensions(JsonElement rootElement, Dimension expected) {

JsonElement frames = rootElement.getAsJsonObject().get("frames");

for(Map.Entry<String, JsonElement> entry : frames.getAsJsonObject().entrySet()) {
JsonObject frameObject = entry.getValue().getAsJsonObject();
JsonObject frameDimensions = frameObject.get("sourceSize").getAsJsonObject();

int frameWidth = frameDimensions.get("w").getAsInt();
int frameHeight = frameDimensions.get("h").getAsInt();

if(frameWidth != expected.getWidth() || frameHeight != expected.getHeight())
return false;
}

return true;
}

/**
* @param rootElement root element of JSON data.
*
* @return integer array representing duration of each key frame.
* */
public static int[] getKeyFrameDurations(JsonElement rootElement) {

JsonElement frames = rootElement.getAsJsonObject().get("frames");

Set<Map.Entry<String, JsonElement>> keyFrameSet = frames.getAsJsonObject().entrySet();

int[] keyFrameDurations = new int[keyFrameSet.size()];

int frameIndex = 0;
for(Map.Entry<String, JsonElement> entry : keyFrameSet) {
JsonObject frameObject = entry.getValue().getAsJsonObject();
int frameDuration = frameObject.get("duration").getAsInt();
keyFrameDurations[frameIndex++] = frameDuration;
}

return keyFrameDurations;
}

/**
* Error that is thrown by the export class
*/
Expand Down Expand Up @@ -207,6 +367,4 @@ public Layer(String name, int opacity, String blendMode){
}

}
}


}
Original file line number Diff line number Diff line change
@@ -1,32 +1,92 @@
package de.gurkenlabs.litiengine.graphics.animation;

import java.io.IOException;
import java.io.FileNotFoundException;
import java.awt.image.BufferedImage;

import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

import de.gurkenlabs.litiengine.graphics.Spritesheet;
import de.gurkenlabs.litiengine.graphics.animation.Animation;
import de.gurkenlabs.litiengine.graphics.animation.AsepriteHandler.ImportAnimationException;
import de.gurkenlabs.litiengine.resources.ImageFormat;

public class AsepriteHandlerTests {

/**
* Tests that Aseprite animation import works as expected when given valid input.
*/
@Test
public void importAsepriteAnimationTest() {
try {
Animation animation = AsepriteHandler.importAnimation("tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animations/Sprite-0001.json");
assertEquals("Sprite-0001-sheet", animation.getName());
assertEquals(300, animation.getTotalDuration());
for(int keyFrameDuration : animation.getKeyFrameDurations())
assertEquals(100, keyFrameDuration);

Spritesheet spriteSheet = animation.getSpritesheet();
assertEquals(32, spriteSheet.getSpriteHeight());
assertEquals(32, spriteSheet.getSpriteWidth());
assertEquals(3, spriteSheet.getTotalNumberOfSprites());
assertEquals(1, spriteSheet.getRows());
assertEquals(3, spriteSheet.getColumns());
assertEquals(ImageFormat.PNG, spriteSheet.getImageFormat());

BufferedImage image = spriteSheet.getImage();
assertEquals(96, image.getWidth());
assertEquals(32, image.getHeight());
}
catch(FileNotFoundException e) {
fail(e.getMessage());
}
catch(IOException e) {
fail(e.getMessage());
}
catch(AsepriteHandler.ImportAnimationException e) {
fail(e.getMessage());
}
}

/**
* Test that if AsepriteHandler.ImportAnimationException will be throwed if different frame dimensions are provided.
*/
@Test
public void ImportAnimationExceptionTest() {

Throwable exception = assertThrows(ImportAnimationException.class, () -> AsepriteHandler.importAnimation("tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animations/Sprite-0002.json"));
assertEquals("AsepriteHandler.ImportAnimationException: animation key frames require same dimensions.", exception.getMessage());
}

/**
* Tests thrown FileNotFoundException when importing an Aseprite animation.
*
* 1.first, we test if FileNotFoundException would be throwed if .json file cannot be found.
* 2.then we test if FileNotFoundException would be throwed if spritesheet file cannot be found.
*/
@Test
public void FileNotFoundExceptionTest(){
Throwable exception_withoutJsonFile = assertThrows(FileNotFoundException.class, () -> AsepriteHandler.importAnimation("tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animations/Sprite-0003.json"));
assertEquals("FileNotFoundException: Could not find .json file tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animations/Sprite-0003.json", exception_withoutJsonFile.getMessage());
Throwable exception_withoutSpriteSheet = assertThrows(FileNotFoundException.class, () -> AsepriteHandler.importAnimation("tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animations/Sprite-0004.json"));
assertEquals("FileNotFoundException: Could not find sprite sheet file. Expected location is 'image' in .json metadata, which evaluates to: tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animations/Sprite-0002-sheet.png", exception_withoutSpriteSheet.getMessage());
}

/**
* Test that just create a json and prints in to standard output.
*/
@Test
public void exportAnimationTest() {
String spritesheetPath = "C:/Users/Nikla/Documents/Programmering/SoftwareFundamentals/Assignment-3-EC/litiengine/tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animation/Sprite-0001-sheet.png";
String spritesheetPath = "tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animations/Sprite-0001-sheet.png";
BufferedImage image = new BufferedImage(96, 32, BufferedImage.TYPE_4BYTE_ABGR);
Spritesheet spritesheet = new Spritesheet(image, spritesheetPath, 32, 32);
Animation animation = new Animation(spritesheet, false, false, 2,2,2);

AsepriteHandler aseprite = new AsepriteHandler();
String result = aseprite.exportAnimation(animation);
System.out.println(result);

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{ "frames": {
"Sprite-0001 0.png": {
"frame": { "x": 0, "y": 0, "w": 32, "h": 32 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
"sourceSize": { "w": 32, "h": 32 },
"duration": 100
},
"Sprite-0001 1.png": {
"frame": { "x": 32, "y": 0, "w": 32, "h": 32 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
"sourceSize": { "w": 32, "h": 32 },
"duration": 100
},
"Sprite-0001 2.png": {
"frame": { "x": 64, "y": 0, "w": 32, "h": 32 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
"sourceSize": { "w": 32, "h": 32 },
"duration": 100
}
},
"meta": {
"app": "http://www.aseprite.org/",
"version": "1.1.9-dev",
"image": "tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animations/Sprite-0001-sheet.png",
"format": "RGBA8888",
"size": { "w": 96, "h": 32 },
"scale": "1",
"frameTags": [
],
"layers": [
{ "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{ "frames": {
"Sprite-0002 0.png": {
"frame": { "x": 0, "y": 0, "w": 32, "h": 32 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
"sourceSize": { "w": 32, "h": 32 },
"duration": 100
},
"Sprite-0002 1.png": {
"frame": { "x": 32, "y": 0, "w": 32, "h": 32 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
"sourceSize": { "w": 64, "h": 64 },
"duration": 100
},
"Sprite-0002 2.png": {
"frame": { "x": 64, "y": 0, "w": 32, "h": 32 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
"sourceSize": { "w": 32, "h": 32 },
"duration": 100
}
},
"meta": {
"app": "http://www.aseprite.org/",
"version": "1.1.9-dev",
"image": "tests/de/gurkenlabs/litiengine/graphics/animation/aseprite_test_animations/Sprite-0001-sheet.png",
"format": "RGBA8888",
"size": { "w": 96, "h": 32 },
"scale": "1",
"frameTags": [
],
"layers": [
{ "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
]
}
}

Loading

0 comments on commit 2163073

Please sign in to comment.