Skip to content

Commit 34842a5

Browse files
authored
[release/3.6] 修复无法加载 WebP 图标的问题 (#4436)
#4171
1 parent 6dd7d53 commit 34842a5

File tree

5 files changed

+155
-78
lines changed

5 files changed

+155
-78
lines changed

HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java

Lines changed: 100 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import javafx.beans.WeakListener;
2828
import javafx.beans.property.BooleanProperty;
2929
import javafx.beans.property.Property;
30+
import javafx.beans.property.SimpleObjectProperty;
3031
import javafx.beans.value.*;
3132
import javafx.event.Event;
3233
import javafx.event.EventDispatcher;
@@ -54,9 +55,8 @@
5455
import javafx.util.Callback;
5556
import javafx.util.Duration;
5657
import javafx.util.StringConverter;
57-
import org.glavo.png.PNGType;
58-
import org.glavo.png.PNGWriter;
59-
import org.glavo.png.javafx.PNGJavaFXUtils;
58+
import org.jackhuang.hmcl.task.FileDownloadTask;
59+
import org.jackhuang.hmcl.task.Schedulers;
6060
import org.jackhuang.hmcl.task.Task;
6161
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
6262
import org.jackhuang.hmcl.util.*;
@@ -78,11 +78,16 @@
7878
import javax.xml.parsers.DocumentBuilder;
7979
import javax.xml.parsers.DocumentBuilderFactory;
8080
import javax.xml.parsers.ParserConfigurationException;
81+
import java.awt.image.BufferedImage;
8182
import java.io.*;
8283
import java.lang.ref.WeakReference;
8384
import java.net.*;
85+
import java.nio.ByteBuffer;
86+
import java.nio.channels.Channels;
87+
import java.nio.channels.FileChannel;
8488
import java.nio.file.Files;
8589
import java.nio.file.Path;
90+
import java.nio.file.StandardOpenOption;
8691
import java.util.List;
8792
import java.util.*;
8893
import java.util.concurrent.ConcurrentHashMap;
@@ -779,6 +784,21 @@ private static Image loadWebPImage(InputStream input) throws IOException {
779784
}
780785
}
781786

787+
public static Image loadWebPImage(InputStream input,
788+
int requestedWidth, int requestedHeight,
789+
boolean preserveRatio, boolean smooth) throws IOException {
790+
WebPImageReaderSpi spi = new WebPImageReaderSpi();
791+
ImageReader reader = spi.createReaderInstance(null);
792+
BufferedImage bufferedImage;
793+
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
794+
reader.setInput(imageInput, true, true);
795+
bufferedImage = reader.read(0, reader.getDefaultReadParam());
796+
} finally {
797+
reader.dispose();
798+
}
799+
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
800+
}
801+
782802
public static Image loadImage(Path path) throws Exception {
783803
try (InputStream input = Files.newInputStream(path)) {
784804
if ("webp".equalsIgnoreCase(FileUtils.getExtension(path)))
@@ -792,6 +812,42 @@ public static Image loadImage(Path path) throws Exception {
792812
}
793813
}
794814

815+
public static Image loadImage(Path path,
816+
int requestedWidth, int requestedHeight,
817+
boolean preserveRatio, boolean smooth) throws Exception {
818+
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
819+
String ext = FileUtils.getExtension(path).toLowerCase(Locale.ROOT);
820+
if ("webp".equalsIgnoreCase(ext))
821+
return loadWebPImage(Channels.newInputStream(channel),
822+
requestedWidth, requestedHeight, preserveRatio, smooth);
823+
824+
if (!IMAGE_EXTENSIONS.contains(ext)) {
825+
byte[] header = new byte[12];
826+
ByteBuffer buffer = ByteBuffer.wrap(header);
827+
//noinspection StatementWithEmptyBody
828+
while (buffer.hasRemaining() && channel.read(buffer) > 0) {
829+
}
830+
831+
channel.position(0L);
832+
if (!buffer.hasRemaining()) {
833+
// WebP File
834+
if (header[0] == 'R' && header[1] == 'I' && header[2] == 'F' && header[3] == 'F' &&
835+
header[8] == 'W' && header[9] == 'E' && header[10] == 'B' && header[11] == 'P') {
836+
return loadWebPImage(Channels.newInputStream(channel),
837+
requestedWidth, requestedHeight, preserveRatio, smooth);
838+
}
839+
}
840+
}
841+
842+
Image image = new Image(Channels.newInputStream(channel),
843+
requestedWidth, requestedHeight,
844+
preserveRatio, smooth);
845+
if (image.isError())
846+
throw image.getException();
847+
return image;
848+
}
849+
}
850+
795851
public static Image loadImage(URL url) throws Exception {
796852
URLConnection connection = NetworkUtils.createConnection(url);
797853
if (connection instanceof HttpURLConnection) {
@@ -851,15 +907,37 @@ public static Image newBuiltinImage(String url, double requestedWidth, double re
851907
}
852908
}
853909

854-
/**
855-
* Load image from the internet. It will cache the data of images for the further usage.
856-
* The cached data will be deleted when HMCL is closed or hidden.
857-
*
858-
* @param url the url of image. The image resource should be a file on the internet.
859-
* @return the image resource within the jar.
860-
*/
861-
public static Image newRemoteImage(String url) {
862-
return newRemoteImage(url, 0, 0, false, false, false);
910+
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
911+
return Task.composeAsync(() -> {
912+
Path currentPath = remoteImageCache.get(url);
913+
if (currentPath != null) {
914+
if (Files.isReadable(currentPath))
915+
return Task.completed(currentPath);
916+
917+
// The file is unavailable or unreadable.
918+
remoteImageCache.remove(url);
919+
920+
try {
921+
Files.deleteIfExists(currentPath);
922+
} catch (IOException e) {
923+
LOG.warning("An exception encountered while deleting broken cached image file.", e);
924+
}
925+
}
926+
927+
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
928+
return new FileDownloadTask(NetworkUtils.toURL(url), newPath.toFile())
929+
.thenSupplyAsync(() -> {
930+
Path otherPath = remoteImageCache.putIfAbsent(url, newPath);
931+
if (otherPath == null)
932+
return newPath;
933+
else {
934+
// The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
935+
Files.delete(newPath);
936+
return otherPath;
937+
}
938+
939+
});
940+
}).thenApplyAsync(path -> loadImage(path, requestedWidth, requestedHeight, preserveRatio, smooth));
863941
}
864942

865943
/**
@@ -877,52 +955,17 @@ public static Image newRemoteImage(String url) {
877955
* the specified bounding box
878956
* @return the image resource within the jar.
879957
*/
880-
public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) {
881-
Path currentPath = remoteImageCache.get(url);
882-
if (currentPath != null) {
883-
if (Files.isReadable(currentPath)) {
884-
try (InputStream inputStream = Files.newInputStream(currentPath)) {
885-
return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth);
886-
} catch (IOException e) {
887-
LOG.warning("An exception encountered while reading data from cached image file.", e);
888-
}
889-
}
890-
891-
// The file is unavailable or unreadable.
892-
remoteImageCache.remove(url);
893-
894-
try {
895-
Files.deleteIfExists(currentPath);
896-
} catch (IOException e) {
897-
LOG.warning("An exception encountered while deleting broken cached image file.", e);
898-
}
899-
}
900-
901-
Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading);
902-
image.progressProperty().addListener((observable, oldValue, newValue) -> {
903-
if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) {
904-
Task.runAsync(() -> {
905-
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
906-
try ( // Make sure the file is released from JVM before we put the path into remoteImageCache.
907-
OutputStream outputStream = Files.newOutputStream(newPath);
908-
PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL)
909-
) {
910-
writer.write(PNGJavaFXUtils.asArgbImage(image));
911-
} catch (IOException e) {
912-
try {
913-
Files.delete(newPath);
914-
} catch (IOException e2) {
915-
e2.addSuppressed(e);
916-
throw e2;
917-
}
918-
throw e;
919-
}
920-
if (remoteImageCache.putIfAbsent(url, newPath) != null) {
921-
Files.delete(newPath); // The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
958+
public static ObservableValue<Image> newRemoteImage(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
959+
SimpleObjectProperty<Image> image = new SimpleObjectProperty<>();
960+
getRemoteImageTask(url, requestedWidth, requestedHeight, preserveRatio, smooth)
961+
.whenComplete(Schedulers.javafx(), (result, exception) -> {
962+
if (exception == null) {
963+
image.set(result);
964+
} else {
965+
LOG.warning("An exception encountered while loading remote image: " + url, exception);
922966
}
923-
}).start();
924-
}
925-
});
967+
})
968+
.start();
926969
return image;
927970
}
928971

HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import javafx.scene.image.ImageView;
2323
import javafx.scene.text.Text;
2424
import javafx.scene.text.TextFlow;
25+
import org.jackhuang.hmcl.util.StringUtils;
2526
import org.jsoup.nodes.Node;
2627
import org.jsoup.nodes.TextNode;
2728

@@ -159,28 +160,19 @@ private void appendAutoLineBreak(String text) {
159160

160161
private void appendImage(Node node) {
161162
String src = node.absUrl("src");
162-
URI imageUri = null;
163-
try {
164-
if (!src.isEmpty())
165-
imageUri = URI.create(src);
166-
} catch (Exception ignored) {
167-
}
168-
169163
String alt = node.attr("alt");
170164

171-
if (imageUri != null) {
172-
URI uri = URI.create(src);
173-
165+
if (StringUtils.isNotBlank(src)) {
174166
String widthAttr = node.attr("width");
175167
String heightAttr = node.attr("height");
176168

177-
double width = 0;
178-
double height = 0;
169+
int width = 0;
170+
int height = 0;
179171

180172
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
181173
try {
182-
width = Double.parseDouble(widthAttr);
183-
height = Double.parseDouble(heightAttr);
174+
width = (int) Double.parseDouble(widthAttr);
175+
height = (int) Double.parseDouble(heightAttr);
184176
} catch (NumberFormatException ignored) {
185177
}
186178

@@ -190,10 +182,12 @@ private void appendImage(Node node) {
190182
}
191183
}
192184

193-
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
194-
if (image.isError()) {
195-
LOG.warning("Failed to load image: " + uri, image.getException());
196-
} else {
185+
try {
186+
Image image = FXUtils.getRemoteImageTask(src, width, height, true, true)
187+
.run();
188+
if (image == null)
189+
throw new AssertionError("Image loading task returned null");
190+
197191
ImageView imageView = new ImageView(image);
198192
if (hyperlink != null) {
199193
URI target = resolveLink(hyperlink);
@@ -204,6 +198,8 @@ private void appendImage(Node node) {
204198
}
205199
children.add(imageView);
206200
return;
201+
} catch (Throwable e) {
202+
LOG.warning("Failed to load image: " + src, e);
207203
}
208204
}
209205

HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ protected void updateControl(RemoteMod dataItem, boolean empty) {
542542
.collect(Collectors.toList()));
543543

544544
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
545-
imageView.setImage(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true, true));
545+
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
546546
}
547547
}
548548
});

HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ protected ModDownloadPageSkin(DownloadPage control) {
220220
{
221221
ImageView imageView = new ImageView();
222222
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
223-
imageView.setImage(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true));
223+
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true));
224224
}
225225
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
226226

@@ -359,7 +359,7 @@ private static final class DependencyModItem extends StackPane {
359359
.collect(Collectors.toList()));
360360

361361
if (StringUtils.isNotBlank(addon.getIconUrl())) {
362-
imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true));
362+
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true));
363363
}
364364
} else {
365365
content.setTitle(i18n("mods.broken_dependency.title"));

HMCL/src/main/java/org/jackhuang/hmcl/util/SwingFXUtils.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,43 @@ public static WritableImage toFXImage(BufferedImage bimg, WritableImage wimg) {
120120
pw.setPixels(0, 0, bw, bh, pf, data, offset, scan);
121121
return wimg;
122122
}
123+
124+
public static WritableImage toFXImage(BufferedImage bimg, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
125+
if (requestedWidth <= 0. || requestedHeight <= 0.) {
126+
return toFXImage(bimg, null);
127+
}
128+
129+
int width = (int) requestedWidth;
130+
int height = (int) requestedHeight;
131+
132+
// Calculate actual dimensions if preserveRatio is true
133+
if (preserveRatio) {
134+
double originalWidth = bimg.getWidth();
135+
double originalHeight = bimg.getHeight();
136+
double scaleX = requestedWidth / originalWidth;
137+
double scaleY = requestedHeight / originalHeight;
138+
double scale = Math.min(scaleX, scaleY);
139+
140+
width = (int) (originalWidth * scale);
141+
height = (int) (originalHeight * scale);
142+
}
143+
144+
// Create scaled BufferedImage
145+
BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
146+
Graphics2D g2d = scaledImage.createGraphics();
147+
try {
148+
if (smooth) {
149+
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
150+
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
151+
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
152+
}
153+
154+
g2d.drawImage(bimg, 0, 0, width, height, null);
155+
} finally {
156+
g2d.dispose();
157+
}
158+
159+
return toFXImage(scaledImage, null);
160+
}
123161
}
124162

0 commit comments

Comments
 (0)