Skip to content

Commit

Permalink
Add support for animated GIF using ImageIO
Browse files Browse the repository at this point in the history
  • Loading branch information
Vinrobot committed Nov 19, 2023
1 parent d2ffbc5 commit 722711f
Show file tree
Hide file tree
Showing 22 changed files with 432 additions and 103 deletions.
11 changes: 4 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,17 @@ dependencies {
// To change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
modImplementation "net.fabricmc:fabric-loader:${project.fabric_loader_version}"

// Uncomment the following line to enable the deprecated Fabric API modules.
// These are included in the Fabric API production distribution and allow you to update your mod to the latest modules at a later more convenient time.

// modImplementation "net.fabricmc.fabric-api:fabric-api-deprecated:${project.fabric_version}"
// modImplementation "net.fabricmc.fabric-api:fabric-api-deprecated:${project.fabric_api_version}"

modClientCompileOnly "com.terraformersmc:modmenu:7.2.1"

// WebP support for ImageIO, not fully working with animated WebP
modClientImplementation "com.twelvemonkeys.imageio:imageio-webp:3.9.4"
modClientCompileOnly "com.terraformersmc:modmenu:${project.modmenu_version}"

// WebP support for animated WebP using official Google's native library
modClientImplementation "com.github.Vinrobot.WebPDecoderJN:lib:1.3"
modClientImplementation "com.github.Vinrobot.WebPDecoderJN:lib:${project.webpdecoderjn_version}"
}

processResources {
Expand Down
14 changes: 9 additions & 5 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@
org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true

# Fabric Properties
# check these on https://fabricmc.net/develop
# Minecraft Properties
minecraft_version=1.20
yarn_mappings=1.20+build.1
loader_version=0.14.21

# Mod Properties
mod_version=1.1.0
maven_group=net.vinrobot
archives_base_name=mcemote

# Dependencies
fabric_version=0.83.0+1.20
# Fabric Properties
# check these on https://fabricmc.net/develop
fabric_loader_version=0.14.21
fabric_api_version=0.83.0+1.20

# Dependencies versions
modmenu_version=7.2.1
webpdecoderjn_version=1.4
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
import net.vinrobot.mcemote.client.text.EmotesManager;
import net.vinrobot.mcemote.config.Configuration;
import net.vinrobot.mcemote.config.ConfigurationManager;
import webpdecoderjn.WebPLoader;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
Expand All @@ -21,13 +19,6 @@ public class MinecraftEmoteModClient implements ClientModInitializer {

@Override
public void onInitializeClient() {
// This entrypoint is suitable for setting up client-specific logic, such as rendering.
try {
WebPLoader.init();
} catch (IOException e) {
MinecraftEmoteMod.LOGGER.error("Failed to initialize WebPDecoder", e);
}

final ConfigurationManager configManager = MinecraftEmote.getInstance().getConfigManager();

configManager.onChange((config) -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package net.vinrobot.mcemote.client.font;

import net.minecraft.client.font.Glyph;

import java.time.Duration;
import java.time.Instant;
import java.util.stream.Stream;
import net.minecraft.client.font.Glyph;

public class AnimatedGlyph {
private final Frame[] frames;
Expand All @@ -14,6 +15,7 @@ public AnimatedGlyph(Frame[] frames) {
this.loopTime = Stream.of(frames)
.map(Frame::duration)
.reduce(Duration::plus)
.filter(d -> !d.isZero())
.orElse(Duration.ofDays(1));
}

Expand All @@ -23,6 +25,10 @@ private static Duration modulo(Instant a, Duration b) {
}

public Glyph getGlyphAt(Instant at) {
if (this.frames.length == 1) {
return this.frames[0].image();
}

final Duration time = modulo(at, loopTime);

Duration current = Duration.ZERO;
Expand Down
11 changes: 2 additions & 9 deletions src/client/java/net/vinrobot/mcemote/client/font/Emote.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package net.vinrobot.mcemote.client.font;

import net.minecraft.client.texture.NativeImage;
import net.vinrobot.mcemote.client.imageio.NativeFrame;

import java.io.IOException;
import java.time.Duration;

public interface Emote {
String getName();
Expand All @@ -12,11 +11,5 @@ public interface Emote {

int getHeight();

Frame[] loadFrames() throws IOException;

record Frame(NativeImage image, Duration duration) {
public Frame(NativeImage image) {
this(image, Duration.ofDays(1));
}
}
NativeFrame[] loadFrames() throws IOException;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
package net.vinrobot.mcemote.client.font;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.font.BuiltinEmptyGlyph;
Expand All @@ -18,8 +10,18 @@
import net.minecraft.util.Identifier;
import net.vinrobot.mcemote.MinecraftEmoteMod;
import net.vinrobot.mcemote.client.helpers.FutureHelper;
import net.vinrobot.mcemote.client.imageio.NativeFrame;
import net.vinrobot.mcemote.client.text.EmotesManager;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Environment(EnvType.CLIENT)
public class EmoteFontStorage extends FontStorage {
public static final Identifier IDENTIFIER = new Identifier("mcemote.fonts", "emotes");
Expand Down Expand Up @@ -58,11 +60,11 @@ private AnimatedGlyph loadAnimatedGlyph(int codePoint) {
final int height = emote.getHeight();
final float advance = width * GLYPH_HEIGHT / height;
final float oversample = height / GLYPH_HEIGHT;
final Emote.Frame[] frames = emote.loadFrames();
final NativeFrame[] frames = emote.loadFrames();

final AnimatedGlyph.Frame[] animatedFrames = new AnimatedGlyph.Frame[frames.length];
for (int i = 0; i < frames.length; i++) {
final Emote.Frame frame = frames[i];
final NativeFrame frame = frames[i];
final Glyph glyph = new NativeImageGlyph(frame.image(), advance, oversample);
animatedFrames[i] = new AnimatedGlyph.Frame(glyph, frame.duration());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.vinrobot.mcemote.client.imageio;

import java.awt.image.BufferedImage;
import java.time.Duration;

public record BufferedFrame(
BufferedImage image,
Duration duration
) {
NativeFrame toNativeFrame() {
return new NativeFrame(NativeImageHelper.fromBufferedImage(image), duration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.vinrobot.mcemote.client.imageio;

import net.minecraft.client.texture.NativeImage;

import java.time.Duration;

public record NativeFrame(
NativeImage image,
Duration duration
) {
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package net.vinrobot.mcemote.client.helpers;
package net.vinrobot.mcemote.client.imageio;

import net.minecraft.client.texture.NativeImage;

import java.awt.image.BufferedImage;
import java.awt.image.Raster;

public final class NativeImageHelper {
public static NativeImage fromBufferedImage(BufferedImage bufferedImage) throws UnsupportedOperationException {
public static NativeImage fromBufferedImage(final BufferedImage bufferedImage) throws UnsupportedOperationException {
final Raster raster = bufferedImage.getTile(0, 0);

final NativeImage.Format imageFormat = switch (raster.getNumBands()) {
Expand All @@ -20,10 +20,10 @@ public static NativeImage fromBufferedImage(BufferedImage bufferedImage) throws
final NativeImage nativeImage = new NativeImage(imageFormat, width, height, false);

// PERF: find a way to transfer directly
int[] pixel = new int[]{0, 0, 0, 255/*ALPHA*/};
final int[] pixel = new int[]{0, 0, 0, 255/*ALPHA*/};
for (int u = 0; u < height; ++u) {
for (int v = 0; v < width; ++v) {
pixel = raster.getPixel(v, u, pixel);
raster.getPixel(v, u, pixel);
nativeImage.setColor(v, u, pixel[0] | (pixel[1] << 8) | (pixel[2] << 16) | (pixel[3] << 24));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package net.vinrobot.mcemote.client.imageio;

import net.vinrobot.mcemote.client.imageio.plugins.gif.GifReader;
import net.vinrobot.mcemote.client.imageio.plugins.webp.WebPReader;

import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.time.Duration;
import java.util.List;

public class NativeImageIO {
public static NativeFrame[] readAll(final URL url) throws IOException {
try (final InputStream input = url.openStream()) {
return readAll(input);
}
}

public static NativeFrame[] readAll(final InputStream input) throws IOException {
return readBufferedFrames(input).stream().map(BufferedFrame::toNativeFrame).toArray(NativeFrame[]::new);
}

private static List<BufferedFrame> readBufferedFrames(final InputStream input) throws IOException {
try (final ImageInputStream stream = ImageIO.createImageInputStream(input)) {
if (stream == null) {
throw new IIOException("Can't create an ImageInputStream!");
}

final ImageReader reader = ImageIO.getImageReaders(stream).next();
try {
reader.setInput(stream, true, false);
return switch (reader.getFormatName()) {
case "gif" -> GifReader.readFrames(reader);
case "webp" -> WebPReader.readFrames(reader);
default -> List.of(new BufferedFrame(reader.read(0), Duration.ofDays(1)));
};
} finally {
reader.dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.vinrobot.mcemote.client.imageio.plugins.gif;

public enum DisposalMethod {
RESTORE_TO_BACKGROUND("restoreToBackgroundColor"),
RESTORE_TO_PREVIOUS("restoreToPrevious"),
DO_NOT_DISPOSE("doNotDispose"),
NONE("none"),
UNSPECIFIED(null);

private final String identifier;

DisposalMethod(final String identifier) {
this.identifier = identifier;
}

public static DisposalMethod getByIdentifier(String identifier) {
for (DisposalMethod method : values()) {
if (method.identifier.equals(identifier)) {
return method;
}
}
return UNSPECIFIED;
}
}
Loading

0 comments on commit 722711f

Please sign in to comment.