Skip to content
Draft
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
22 changes: 22 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/image/AnimationFrame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.image;

public interface AnimationFrame {
long getDuration();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,84 @@
*/
package org.jackhuang.hmcl.ui.image;

import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.value.WritableValue;
import javafx.scene.image.WritableImage;
import javafx.util.Duration;

import java.lang.ref.WeakReference;

/**
* @author Glavo
*/
public interface AnimationImage {
public abstract class AnimationImage extends WritableImage {
private Animation animation;
protected final int cycleCount;
protected final int width;
protected final int height;

public AnimationImage(int width, int height, int cycleCount) {
super(width, height);
this.cycleCount = cycleCount;
this.width = width;
this.height = height;
}

public void play() {
if (animation == null) {
animation = new Animation(this);
animation.timeline.play();
}
}

public abstract int getFramesCount();

public abstract long getDuration(int index);

protected abstract void updateImage(int frameIndex);

private static final class Animation implements WritableValue<Integer> {
private final Timeline timeline = new Timeline();
private final WeakReference<AnimationImage> imageRef;

private Integer value;

private Animation(AnimationImage image) {
this.imageRef = new WeakReference<>(image);
timeline.setCycleCount(image.cycleCount);

long duration = 0;

int frames = image.getFramesCount();
for (int i = 0; i < frames; ++i) {
timeline.getKeyFrames().add(
new KeyFrame(Duration.millis(duration),
new KeyValue(this, i, Interpolator.DISCRETE)));

duration = duration + image.getDuration(i);
}

timeline.getKeyFrames().add(new KeyFrame(Duration.millis(duration)));
}

@Override
public Integer getValue() {
return value;
}

@Override
public void setValue(Integer value) {
this.value = value;

AnimationImage image = imageRef.get();
if (image == null) {
timeline.stop();
return;
}
image.updateImage(value);
}
}
}
214 changes: 7 additions & 207 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,11 @@
package org.jackhuang.hmcl.ui.image;

import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import javafx.animation.Timeline;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import org.jackhuang.hmcl.ui.image.apng.Png;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngAnimationControl;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
import org.jackhuang.hmcl.ui.image.apng.argb8888.*;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl;
import org.jackhuang.hmcl.ui.image.apng.reader.DefaultPngChunkReader;
import org.jackhuang.hmcl.ui.image.apng.reader.PngReadHelper;
import org.jackhuang.hmcl.util.SwingFXUtils;
import org.jetbrains.annotations.Nullable;

Expand All @@ -42,8 +35,6 @@
import java.util.*;
import java.util.regex.Pattern;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;

/**
* @author Glavo
*/
Expand Down Expand Up @@ -78,57 +69,10 @@ public final class ImageUtils {
return DEFAULT.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);

try {
var sequence = Png.readArgb8888BitmapSequence(input);

final int width = sequence.header.width;
final int height = sequence.header.height;

boolean doScale;
if (requestedWidth > 0 && requestedHeight > 0
&& (requestedWidth != width || requestedHeight != height)) {
doScale = true;

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

requestedWidth = (int) (width * scale);
requestedHeight = (int) (height * scale);
}
} else {
doScale = false;
}

if (sequence.isAnimated()) {
try {
return toImage(sequence, doScale, requestedWidth, requestedHeight);
} catch (Throwable e) {
LOG.warning("Failed to load animated image", e);
}
}

Argb8888Bitmap defaultImage = sequence.defaultImage;
int targetWidth;
int targetHeight;
int[] pixels;
if (doScale) {
targetWidth = requestedWidth;
targetHeight = requestedHeight;
pixels = scale(defaultImage.array,
defaultImage.width, defaultImage.height,
targetWidth, targetHeight);
} else {
targetWidth = defaultImage.width;
targetHeight = defaultImage.height;
pixels = defaultImage.array;
}

WritableImage image = new WritableImage(targetWidth, targetHeight);
image.getPixelWriter().setPixels(0, 0, targetWidth, targetHeight,
PixelFormat.getIntArgbInstance(), pixels,
0, targetWidth);
return image;
return PngReadHelper.read(input, new DefaultPngChunkReader<>(
new Argb8888Processor<>(
new BgraPreBitmapDirector(
requestedWidth, requestedHeight, preserveRatio, smooth))));
} catch (PngException e) {
throw new IOException(e);
}
Expand Down Expand Up @@ -262,150 +206,6 @@ private static int[] scale(int[] pixels,
return result;
}

private static Image toImage(Argb8888BitmapSequence sequence,
boolean doScale,
int targetWidth, int targetHeight) throws PngException {
final int width = sequence.header.width;
final int height = sequence.header.height;

List<Argb8888BitmapSequence.Frame> frames = sequence.getAnimationFrames();

var framePixels = new int[frames.size()][];
var durations = new int[framePixels.length];

int[] buffer = new int[Math.multiplyExact(width, height)];
for (int frameIndex = 0; frameIndex < frames.size(); frameIndex++) {
var frame = frames.get(frameIndex);
PngFrameControl control = frame.control;

if (frameIndex == 0 && (
control.xOffset != 0 || control.yOffset != 0
|| control.width != width || control.height != height)) {
throw new PngIntegrityException("Invalid first frame: " + control);
}

if (control.xOffset < 0 || control.yOffset < 0
|| width < 0 || height < 0
|| control.xOffset + control.width > width
|| control.yOffset + control.height > height
|| control.delayNumerator < 0 || control.delayDenominator < 0
) {
throw new PngIntegrityException("Invalid frame control: " + control);
}

int[] currentFrameBuffer = buffer.clone();
if (control.blendOp == 0) {
for (int row = 0; row < control.height; row++) {
System.arraycopy(frame.bitmap.array,
row * control.width,
currentFrameBuffer,
(control.yOffset + row) * width + control.xOffset,
control.width);
}
} else if (control.blendOp == 1) {
// APNG_BLEND_OP_OVER - Alpha blending
for (int row = 0; row < control.height; row++) {
for (int col = 0; col < control.width; col++) {
int srcIndex = row * control.width + col;
int dstIndex = (control.yOffset + row) * width + control.xOffset + col;

int srcPixel = frame.bitmap.array[srcIndex];
int dstPixel = currentFrameBuffer[dstIndex];

int srcAlpha = (srcPixel >>> 24) & 0xFF;
if (srcAlpha == 0) {
continue;
} else if (srcAlpha == 255) {
currentFrameBuffer[dstIndex] = srcPixel;
} else {
int srcR = (srcPixel >>> 16) & 0xFF;
int srcG = (srcPixel >>> 8) & 0xFF;
int srcB = srcPixel & 0xFF;

int dstAlpha = (dstPixel >>> 24) & 0xFF;
int dstR = (dstPixel >>> 16) & 0xFF;
int dstG = (dstPixel >>> 8) & 0xFF;
int dstB = dstPixel & 0xFF;

int invSrcAlpha = 255 - srcAlpha;

int outAlpha = srcAlpha + (dstAlpha * invSrcAlpha + 127) / 255;
int outR, outG, outB;

if (outAlpha == 0) {
outR = outG = outB = 0;
} else {
outR = (srcR * srcAlpha + dstR * dstAlpha * invSrcAlpha / 255 + outAlpha / 2) / outAlpha;
outG = (srcG * srcAlpha + dstG * dstAlpha * invSrcAlpha / 255 + outAlpha / 2) / outAlpha;
outB = (srcB * srcAlpha + dstB * dstAlpha * invSrcAlpha / 255 + outAlpha / 2) / outAlpha;
}

outAlpha = Math.min(outAlpha, 255);
outR = Math.min(outR, 255);
outG = Math.min(outG, 255);
outB = Math.min(outB, 255);

currentFrameBuffer[dstIndex] = (outAlpha << 24) | (outR << 16) | (outG << 8) | outB;
}
}
}
} else {
throw new PngIntegrityException("Unsupported blendOp " + control.blendOp + " at frame " + frameIndex);
}

if (doScale)
framePixels[frameIndex] = scale(currentFrameBuffer,
width, height,
targetWidth, targetHeight);
else
framePixels[frameIndex] = currentFrameBuffer;

if (control.delayNumerator == 0) {
durations[frameIndex] = 10;
} else {
int durationsMills = 1000 * control.delayNumerator;
if (control.delayDenominator == 0)
durationsMills /= 100;
else
durationsMills /= control.delayDenominator;

durations[frameIndex] = durationsMills;
}

switch (control.disposeOp) {
case 0: // APNG_DISPOST_OP_NONE
System.arraycopy(currentFrameBuffer, 0, buffer, 0, currentFrameBuffer.length);
break;
case 1: // APNG_DISPOSE_OP_BACKGROUND
for (int row = 0; row < control.height; row++) {
int fromIndex = (control.yOffset + row) * width + control.xOffset;
Arrays.fill(buffer, fromIndex, fromIndex + control.width, 0);
}
break;
case 2: // APNG_DISPOSE_OP_PREVIOUS
// Do nothing, keep the previous frame.
break;
default:
throw new PngIntegrityException("Unsupported disposeOp " + control.disposeOp + " at frame " + frameIndex);
}
}

PngAnimationControl animationControl = sequence.getAnimationControl();
int cycleCount;
if (animationControl != null) {
cycleCount = animationControl.numPlays;
if (cycleCount == 0)
cycleCount = Timeline.INDEFINITE;
} else {
cycleCount = Timeline.INDEFINITE;
}

if (doScale)
return new AnimationImageImpl(targetWidth, targetHeight, framePixels, durations, cycleCount);
else
return new AnimationImageImpl(width, height, framePixels, durations, cycleCount);
}

private ImageUtils() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package org.jackhuang.hmcl.ui.image.apng.argb8888;

import org.jackhuang.hmcl.ui.image.apng.Png;
import org.jackhuang.hmcl.ui.image.apng.PngScanlineBuffer;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngAnimationControl;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
Expand Down Expand Up @@ -41,7 +42,7 @@ public interface Argb8888Director<ResultT> {

Argb8888ScanlineProcessor receiveFrameControl(PngFrameControl control);

void receiveFrameImage(Argb8888Bitmap bitmap);
void receiveFrameImage(Argb8888Bitmap bitmap) throws PngException;

ResultT getResult();
}
Loading