Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented import functionality for Aseprite animations #20

Merged
merged 7 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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