Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

优化 APNG 支持 #4230

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

Draft
Glavo wants to merge 17 commits into HMCL-dev:main
base: main
Choose a base branch
Loading
from Glavo:apng
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
aa8f0e4
Create BgraPreCanvas
Glavo Aug 9, 2025
88db57b
update
Glavo Aug 9, 2025
8b3605d
update BgraPreCanvas
Glavo Aug 9, 2025
e03d855
update
Glavo Aug 9, 2025
5830d7c
update
Glavo Aug 9, 2025
a2ff22b
update
Glavo Aug 9, 2025
dae4d3c
update
Glavo Aug 9, 2025
eb51449
update
Glavo Aug 9, 2025
fe0dd17
Update HMCL/src/main/java/org/jackhuang/hmcl/ui/image/internal/BgraPr...
Glavo Aug 14, 2025
d0e9fcf
Update HMCL/src/main/java/org/jackhuang/hmcl/ui/image/internal/BgraPr...
Glavo Aug 14, 2025
1c7d44e
Update HMCL/src/main/java/org/jackhuang/hmcl/ui/image/internal/BgraPr...
Glavo Aug 14, 2025
d805558
Update HMCL/src/main/java/org/jackhuang/hmcl/ui/image/internal/BgraPr...
Glavo Aug 14, 2025
ec5ac5a
Update HMCL/src/main/java/org/jackhuang/hmcl/ui/image/internal/BgraPr...
Glavo Aug 14, 2025
46f4f8f
Update HMCL/src/main/java/org/jackhuang/hmcl/ui/image/internal/BgraPr...
Glavo Aug 14, 2025
cc86e07
update
Glavo Aug 14, 2025
6651dcb
fix
Glavo Aug 14, 2025
805dd2b
Merge remote-tracking branch 'upstream/main' into apng
Glavo Sep 27, 2025
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
View file Open in desktop
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();
}
View file Open in desktop
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
View file Open in desktop
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() {
}
}
View file Open in desktop
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

AltStyle によって変換されたページ (->オリジナル) /