Skip to content
Open
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
1 change: 1 addition & 0 deletions HMCL/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ dependencies {
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.jwebp)
implementation(libs.javif)
implementation(libs.fxsvgimage)
implementation(libs.java.info)
implementation(libs.monet.fx)
Expand Down
130 changes: 130 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import org.girod.javafx.svgimage.ScaleQuality;
import org.glavo.avif.AvifFrame;
import org.glavo.avif.AvifImage;
import org.glavo.avif.AvifImageInfo;
import org.glavo.avif.AvifSequenceInfo;
import org.glavo.avif.javafx.AvifFXImage;
import org.glavo.webp.WebPImage;
import org.glavo.webp.WebPImageLoadOptions;
import org.glavo.webp.javafx.WebPFXImage;
Expand All @@ -42,6 +47,7 @@
import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;

import java.io.IOException;
import java.nio.ByteBuffer;
Expand Down Expand Up @@ -78,6 +84,59 @@ public final class ImageUtils {
return new WebPFXImage(WebPImage.read(input, options));
};

public static final ImageLoader AVIF = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
AvifImage image = AvifImage.read(input);
AvifImageInfo info = image.info();

final int width = info.width();
final int height = info.height();

// A request is only honored when both dimensions are given and at least one differs
// from the decoded size; otherwise the image is returned at its native resolution.
boolean doScale = requestedWidth > 0 && requestedHeight > 0
&& (requestedWidth != width || requestedHeight != height);

int targetWidth = requestedWidth;
int targetHeight = requestedHeight;
if (doScale && preserveRatio) {
double scaleX = (double) requestedWidth / width;
double scaleY = (double) requestedHeight / height;
double scale = Math.min(scaleX, scaleY);

targetWidth = (int) (width * scale);
targetHeight = (int) (height * scale);
}

if (!doScale)
return new AvifFXImage(image);

List<AvifFrame> frames = image.frames();
if (frames.size() > 1) {
@Nullable AvifSequenceInfo sequenceInfo = info.sequenceInfo();
AvifFrame firstFrame = frames.get(0);
int sourceWidth = firstFrame.width();
int sourceHeight = firstFrame.height();

int[][] framePixels = new int[frames.size()][];
for (int i = 0; i < frames.size(); i++) {
framePixels[i] = scale(frames.get(i).intPixels(),
sourceWidth, sourceHeight,
targetWidth, targetHeight);
}

return new AnimationImageImpl(targetWidth, targetHeight, framePixels,
avifFrameDurations(sequenceInfo, frames.size()),
avifCycleCount(sequenceInfo));
}

int[] pixels = scale(image.firstFrame().intPixels(), width, height, targetWidth, targetHeight);

WritableImage result = new WritableImage(targetWidth, targetHeight);
result.getPixelWriter().setPixels(0, 0, targetWidth, targetHeight,
PixelFormat.getIntArgbInstance(), pixels, 0, targetWidth);
return result;
};

public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);

Expand Down Expand Up @@ -181,6 +240,7 @@ public final class ImageUtils {

public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
"webp", WEBP,
"avif", AVIF,
"svg", SVG,
"apng", APNG
);
Expand Down Expand Up @@ -729,6 +789,76 @@ private static int[] rgbaToArgb(ByteBuffer rgba) {
return argb;
}

// AVIF

/// Computes per-frame display durations, in milliseconds, for an AVIF animation.
///
/// When `sequenceInfo` supplies usable per-frame timing (see [#hasUsableAvifSequenceTiming]),
/// that timing is converted to milliseconds; otherwise a uniform 30 frames-per-second
/// fallback is used. This mirrors the behavior of `org.glavo.avif.javafx.AvifFXImage`.
///
/// @param sequenceInfo sequence timing metadata, or `null` if unavailable
/// @param frameCount number of frames in the animation
/// @return an array of `frameCount` frame durations, in milliseconds
private static int @Unmodifiable [] avifFrameDurations(@Nullable AvifSequenceInfo sequenceInfo, int frameCount) {
int[] durations = new int[frameCount];

if (hasUsableAvifSequenceTiming(sequenceInfo, frameCount)) {
// hasUsableAvifSequenceTiming(...) returning true guarantees sequenceInfo is non-null.

int[] frameDurations = sequenceInfo.frameDurations();
int mediaTimescale = sequenceInfo.mediaTimescale();
for (int i = 0; i < frameCount; i++) {
durations[i] = (int) (frameDurations[i] * 1000.0 / mediaTimescale);
}
} else {
Arrays.fill(durations, 1000 / 30);
}

return durations;
}

/// Returns whether `sequenceInfo` supplies per-frame timing that can be trusted to
/// drive an animation.
///
/// @param sequenceInfo sequence timing metadata, or `null` if unavailable
/// @param frameCount number of decoded frames
/// @return whether `sequenceInfo` is non-`null`, has a positive
/// [AvifSequenceInfo#mediaTimescale()], its frame-duration array length matches
/// `frameCount`, and at least one duration is positive
private static boolean hasUsableAvifSequenceTiming(@Nullable AvifSequenceInfo sequenceInfo, int frameCount) {
if (sequenceInfo == null || sequenceInfo.mediaTimescale() <= 0)
return false;

int[] frameDurations = sequenceInfo.frameDurations();
if (frameDurations.length != frameCount)
return false;

for (int frameDuration : frameDurations) {
if (frameDuration > 0)
return true;
}
return false;
}

/// Computes the [Timeline] cycle count corresponding to an AVIF sequence's repeat count.
///
/// @param sequenceInfo sequence timing metadata, or `null` if unavailable
/// @return [Timeline#INDEFINITE] if `sequenceInfo` is `null` or its repeat count is
/// [AvifSequenceInfo#REPETITION_COUNT_UNKNOWN] or [AvifSequenceInfo#REPETITION_COUNT_INFINITE];
/// otherwise the repeat count plus one
private static int avifCycleCount(@Nullable AvifSequenceInfo sequenceInfo) {
if (sequenceInfo == null)
return Timeline.INDEFINITE;

int repetitionCount = sequenceInfo.repetitionCount();
if (repetitionCount == AvifSequenceInfo.REPETITION_COUNT_UNKNOWN
|| repetitionCount == AvifSequenceInfo.REPETITION_COUNT_INFINITE) {
return Timeline.INDEFINITE;
}
return repetitionCount == Integer.MAX_VALUE ? Integer.MAX_VALUE : repetitionCount + 1;
}

/// Prevents instantiation.
private ImageUtils() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
*/
public abstract class Modpack {

public static final Set<String> SUPPORTED_ICON_EXTS = Set.of("png", "jpg", "jpeg", "bmp", "gif", "webp", "apng");
public static final Set<String> SUPPORTED_ICON_NAMES = Set.of("icon.png", "icon.jpg", "icon.jpeg", "icon.bmp", "icon.gif", "icon.webp", "icon.apng");
public static final Set<String> SUPPORTED_ICON_EXTS = Set.of("png", "jpg", "jpeg", "bmp", "gif", "webp", "avif", "apng");
public static final Set<String> SUPPORTED_ICON_NAMES = Set.of("icon.png", "icon.jpg", "icon.jpeg", "icon.bmp", "icon.gif", "icon.webp", "icon.avif", "icon.apng");

private String name;
private String author;
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ monet-fx = "0.4.0"
terracotta = "0.4.2"
nayuki-qrcodegen = "1.8.0"
jwebp = "0.2.0"
javif = "0.1.0"
weburl = "0.2.0"
uuid-tools = "0.2.0"
commonmark = "0.27.1"
Expand Down Expand Up @@ -61,6 +62,7 @@ lwjgl-unsafe-agent = { module = "org.glavo:lwjgl-unsafe-agent", version.ref = "l
monet-fx = { module = "org.glavo:MonetFX", version.ref = "monet-fx" }
nayuki-qrcodegen = { module = "io.nayuki:qrcodegen", version.ref = "nayuki-qrcodegen" }
jwebp = { module = "org.glavo:webp", version.ref = "jwebp" }
javif = { module = "org.glavo:avif", version.ref = "javif" }
weburl = { module = "org.glavo:weburl", version.ref = "weburl" }
uuid-tools = { module = "org.glavo:uuid-tools", version.ref = "uuid-tools" }
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
Expand Down