Compare commits

..
Author SHA1 Message Date
8d876fbff5 [release/3.6] 为不同类型的 Dialog 指定相应的默认标题 (#5671)
https://github.com/HMCL-dev/HMCL/pull/5079
https://github.com/HMCL-dev/HMCL/pull/5152
https://github.com/HMCL-dev/HMCL/pull/5633

Co-authored-by: neveler <55753029+neveler@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-03-23 22:07:21 +08:00
辞庐andGlavo 4c1f15c98c [release/3.6] 修复 CurseForge 源无法搜索到内容的问题 (#5733)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-08 22:13:54 +08:00
3gf8jv4dv e8b9d98c99 [release/3.6] chore: update copyright year and attribution (#5670)
https://github.com/HMCL-dev/HMCL/pull/5622
2026-03-07 21:50:58 +08:00
3gf8jv4dv 61b7eb34f6 [release/3.6] fix(l10n): replace mcedit with minecraft wiki's tutorial page (#5672)
https://github.com/HMCL-dev/HMCL/pull/5524
2026-03-07 21:39:05 +08:00
3gf8jv4dv a2df345b3c [release/3.6] style(l10n): simplify update button text in mod management header (#5673)
https://github.com/HMCL-dev/HMCL/pull/4712
2026-03-07 21:37:56 +08:00
3gf8jv4dv 70030a9558 [release/3.6] fix(i18n): update i18n for open dir button on instance mods tab (#5674)
https://github.com/HMCL-dev/HMCL/pull/4734
2026-03-03 21:45:00 +08:00
Glavoand3gf8jv4dv f03a4ff807 [release/3.6] 添加“提前预览 HMCL 版本”选项 (#4668)
https://github.com/HMCL-dev/HMCL/pull/4223
https://github.com/HMCL-dev/HMCL/pull/4598
https://github.com/HMCL-dev/HMCL/pull/4599

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-10-13 15:38:29 +08:00
Glavo de9a105f2f [release/3.6] Update versions.txt to 1.21.10 (#4667)
https://github.com/HMCL-dev/HMCL/pull/4626
2025-10-13 15:27:48 +08:00
Glavo b5d3653c9a [release/3.6] 移除 Pack200 支持 (#4666)
https://github.com/HMCL-dev/HMCL/pull/4267
2025-10-13 15:23:53 +08:00
Glavo 554503fff0 [release/3.6] 在 CacheRepository.ETagItem 中添加 expires 字段 (#4662) 2025-10-13 15:12:51 +08:00
Glavo 785f334241 [release/3.6] 更新 Jenkins CI 服务器的发布模型 (#4582)
https://github.com/HMCL-dev/HMCL/pull/4559
2025-10-02 20:38:58 +08:00
GlavoandShulkerSakura 0b87959716 [release/3.6] Added 16 characters length detection of username (#4580)
https://github.com/HMCL-dev/HMCL/pull/4088

Co-authored-by: ShulkerSakura <50770360+ShulkerSakura@users.noreply.github.com>
2025-10-01 20:44:11 +08:00
Glavoand8Mi_Yile ae18ca7567 [release/3.6] 更新 unlisted-versions-of-minecraft 镜像源地址 (#4579)
https://github.com/HMCL-dev/HMCL/pull/4120

Co-authored-by: 8Mi_Yile <admin@8mi.tech>
2025-10-01 20:34:56 +08:00
Glavoand3gf8jv4dv 7ebdc783c6 [release/3.6] chore: update versions.txt to 1.21.9 (#4578)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-10-01 20:22:18 +08:00
Glavo 39abdf1ecf [release/3.6] 修复启用代理身份认证且未设置密码时启动器崩溃的问题 (#4536)
https://github.com/HMCL-dev/HMCL/pull/4465
2025-09-23 11:11:19 +08:00
Glavo 8df6563d56 [release/3.6] 修复滚动切换游戏实例/账户功能 (#4535)
https://github.com/HMCL-dev/HMCL/pull/4466
2025-09-22 22:39:38 +08:00
Glavo f62c12fd3c [release/3.6] 优化 Java 自动选择规则 (#4534)
https://github.com/HMCL-dev/HMCL/pull/4515
2025-09-22 22:32:11 +08:00
Glavo 1205212e52 [release/3.6] 修复使用 JavaFX 25 时游戏下载界面布局错误的问题 (#4533)
https://github.com/HMCL-dev/HMCL/pull/4508
2025-09-22 22:25:32 +08:00
Glavo 0d9a275fb3 [release/3.6] 检测到配置文件由高版本启动器创建时禁止保存配置文件 (#4449)
https://github.com/HMCL-dev/HMCL/pull/4414
2025-09-11 01:19:56 +08:00
Glavoand3gf8jv4dv ab72ac3d10 [release/3.6] chore(l10n): change translation from 版本 to 实例 in zh-cn for term INSTANCE (#4448)
https://github.com/HMCL-dev/HMCL/pull/4376

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-09-11 00:24:53 +08:00
Glavoand辞庐 9d5f8a9f0e [release/3.6] 修复 LWJGL 路径显示错误的问题 (#4447)
https://github.com/HMCL-dev/HMCL/pull/4192

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-11 00:15:13 +08:00
GlavoandWulian233 fe2e96345b [release/3.6] 修复世界管理页面不支持显示极限模式的问题 (#4446)
https://github.com/HMCL-dev/HMCL/pull/4321

Co-authored-by: Wulian233 <1055917385@qq.com>
2025-09-11 00:05:23 +08:00
Glavoand辞庐 881fa2858f [release/3.6] 修复整合包格式不匹配提示未被正确格式化的问题 (#4445)
https://github.com/HMCL-dev/HMCL/pull/4176

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-11 00:01:11 +08:00
Glavo a0b95866be [release/3.6] Update microsoft_auth.html (#4444)
https://github.com/HMCL-dev/HMCL/pull/4388
2025-09-10 23:53:35 +08:00
Glavo 93bbef385d [release/3.6] 修复 RemoteMod::getIntegrityCheck 未选择正确的算法的问题 (#4443)
https://github.com/HMCL-dev/HMCL/pull/4238
2025-09-10 23:44:09 +08:00
Glavoand神奇的赵二 9dcc2727aa [release/3.6] 新增内存显示条禁用样式 (#4442)
https://github.com/HMCL-dev/HMCL/pull/4185

Co-authored-by: 神奇的赵二 (OrzMiku) <miku@ecy.pink>
2025-09-10 23:39:12 +08:00
Glavoand辞庐 865c8588d8 [release/3.6] 修复“下载线程数”的指示器被提示覆盖的问题 (#4441)
https://github.com/HMCL-dev/HMCL/pull/4153

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-10 23:34:52 +08:00
3gf8jv4dvand辞庐 419612bd3e [release/3.6] backport language file changes to the 3.6 branch (#4319)
https://github.com/HMCL-dev/HMCL/pull/3690
https://github.com/HMCL-dev/HMCL/pull/4149
https://github.com/HMCL-dev/HMCL/pull/4188
https://github.com/HMCL-dev/HMCL/pull/4189
https://github.com/HMCL-dev/HMCL/pull/4199
https://github.com/HMCL-dev/HMCL/pull/4200
https://github.com/HMCL-dev/HMCL/pull/4303

---------

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-10 23:34:13 +08:00
Glavo d16eded1ad [release/3.6] 弃用 Java 11 支持 (#4422) 2025-09-10 23:24:51 +08:00
Glavo c3a47655f0 [release/3.6] 更新模组和整合包数据 (#4440)
https://github.com/HMCL-dev/HMCL/pull/4439
2025-09-10 23:21:30 +08:00
Glavo 34842a5f26 [release/3.6] 修复无法加载 WebP 图标的问题 (#4436)
https://github.com/HMCL-dev/HMCL/pull/4171
2025-09-10 23:20:27 +08:00
Glavo 6dd7d53c20 [release/3.6] 修复 backgroundType 为 null 时启动器崩溃的问题 (#4438)
https://github.com/HMCL-dev/HMCL/pull/4437
2025-09-10 22:58:18 +08:00
Glavo 1654c652e0 [release/3.6] 在 ./.hmcl/java 中搜索 Java (#4434)
https://github.com/HMCL-dev/HMCL/pull/4179
2025-09-10 21:37:45 +08:00
Glavoand辞庐 881f5e7874 [release/3.6] 模组下载列表翻页后自动回到顶部 (#4433)
https://github.com/HMCL-dev/HMCL/pull/4170

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-10 21:28:03 +08:00
Glavo 73f0eb472a [release/3.6] 修复首次打开子页面时切换动画丢失的问题 (#4431)
https://github.com/HMCL-dev/HMCL/pull/4365
2025-09-10 21:17:00 +08:00
Glavo e13b8385e0 [release/3.6] 在 Windows 7/8/8.1 上使用 llvmpipe-loader 加载 mesa (#4430)
https://github.com/HMCL-dev/HMCL/pull/4224
2025-09-10 21:01:36 +08:00
Glavo a6e4651307 [release/3.6] 修复非 Windows 平台上游戏进程的 user.home 可能被设置为 null 的问题 (#4427)
https://github.com/HMCL-dev/HMCL/pull/4241
2025-09-10 20:55:33 +08:00
Glavo 554e573d7f [release/3.6] 支持通过环境变量强制使用 GPU 渲染和控制动画帧率 (#4426)
https://github.com/HMCL-dev/HMCL/pull/4164
2025-09-10 20:46:51 +08:00
Glavo 3096f933af [release/3.6] 修复导入异常格式的数据包时启动器崩溃的问题 (#4425)
https://github.com/HMCL-dev/HMCL/pull/4152
2025-09-10 20:41:29 +08:00
GlavoandWulian233 ff3b486f58 [release/3.6] 修复游戏版本列表错误地过滤掉部分版本的问题 (#4424)
https://github.com/HMCL-dev/HMCL/pull/4066
https://github.com/HMCL-dev/HMCL/pull/4244

---------

Co-authored-by: Wulian233 <1055917385@qq.com>
2025-09-10 20:36:01 +08:00
GlavoandRoj234 708e0326a7 [release/3.6] 修复多次启动游戏时内存泄露的问题 (#4423)
https://github.com/HMCL-dev/HMCL/pull/4121

Co-authored-by: Roj234 <82699138+roj234@users.noreply.github.com>
2025-09-10 20:27:12 +08:00
GlavoandWulian233 f7a5c7a310 [release/3.6] 修复 1.21.5 以上的地图跳转种子地图错误 (#4317)
https://github.com/HMCL-dev/HMCL/pull/4274

Co-authored-by: Wulian233 <1055917385@qq.com>
2025-08-22 20:48:56 +08:00
Glavo 633e8f8fe2 [release/3.6] Update amdgpu.ids (#4316) 2025-08-22 20:44:36 +08:00
Glavoand辞庐 440ea1b21d [release/3.6] fix: 无法从 BMCLAPI 下载 NeoForge 所有所需文件 (#4313)
https://github.com/HMCL-dev/HMCL/pull/4174

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-08-22 20:36:18 +08:00
320 changed files with 5200 additions and 10270 deletions

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
name: Check Style
on:
push:
paths:
- '**.java'
pull_request:
paths:
- '**.java'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Check style main
run: ./gradlew checkstyleMain --no-daemon
- name: Check style test
run: ./gradlew checkstyleTest --no-daemon
@@ -1,13 +1,11 @@
name: Check Codes
name: Check Translations
on:
push:
paths:
- '**.java'
- '**.properties'
pull_request:
paths:
- '**.java'
- '**.properties'
jobs:
@@ -15,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
java-version: '11'
java-package: 'jdk+fx'
- name: Check Codes
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel
- name: Check style test
run: ./gradlew checkTranslations --no-daemon
+2 -2
View File
@@ -13,11 +13,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
java-version: '11'
java-package: 'jdk+fx'
- name: Build with Gradle
run: ./gradlew build --no-daemon
-2
View File
@@ -13,13 +13,11 @@ hs_err_pid*
/externalgames
NVIDIA
minecraft-exported-crash-info*
hmcl-exported-logs-*
# gradle build
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLBoot/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/buildSrc/build/
+43 -40
View File
@@ -1,4 +1,7 @@
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
@@ -12,45 +15,46 @@ plugins {
alias(libs.plugins.shadow)
}
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
.isNullOrEmpty())
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
if (number != null) {
(number - offset).toString()
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
val prefix = if (isOfficial) "dev" else "unofficial"
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
version = "$versionRoot.$buildNumber"
val embedResources by configurations.registering
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt()
if (buildNumber != null) {
version = if (JenkinsUtils.IS_ON_CI && versionType == "dev") {
"$versionRoot.0.$buildNumber"
} else {
"$versionRoot.$buildNumber"
}
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
version = if (shortCommit.isNullOrBlank()) {
"$versionRoot.SNAPSHOT"
} else if (isOfficial) {
"$versionRoot.dev-$shortCommit"
} else {
"$versionRoot.unofficial-$shortCommit"
}
}
dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
if (launcherExe == null) {
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
}
embedResources(libs.authlib.injector)
}
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
@@ -94,20 +98,18 @@ fun attachSignature(jar: File) {
}
}
tasks.withType<JavaCompile> {
val java11 = sourceSets.create("java11") {
java {
srcDir("src/main/java11")
}
}
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.checkstyleMain {
// Third-party code is not checked
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
}
tasks.compileJava {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
}
tasks.jar {
enabled = false
dependsOn(tasks["shadowJar"])
@@ -125,7 +127,7 @@ tasks.shadowJar {
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
listOf(
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*","freebsd-*", "linux-*", "darwin-*",
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
).forEach { exclude("com/sun/jna/$it/**") }
@@ -133,11 +135,6 @@ tasks.shadowJar {
exclude(dependency("com.google.code.gson:.*:.*"))
exclude(dependency("net.java.dev.jna:jna:.*"))
exclude(dependency("libs:JFoenix:.*"))
exclude(project(":HMCLBoot"))
}
into("assets/") {
from(embedResources)
}
manifest {
@@ -149,7 +146,6 @@ tasks.shadowJar {
"Microsoft-Auth-Id" to microsoftAuthId,
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Authlib-Injector-Version" to libs.authlib.injector.get().version!!,
"Build-Channel" to versionType,
"Add-Opens" to listOf(
"java.base/java.lang",
@@ -174,7 +170,7 @@ tasks.shadowJar {
}
}
if (launcherExe != null) {
if (launcherExe.isNotBlank()) {
into("assets") {
from(file(launcherExe))
}
@@ -186,6 +182,13 @@ tasks.shadowJar {
}
}
tasks.processResources {
into("META-INF/versions/11") {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
}
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

@@ -19,7 +19,6 @@ package org.jackhuang.hmcl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
@@ -28,7 +27,6 @@ import javafx.scene.input.DataFormat;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
@@ -56,8 +54,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
@@ -82,20 +78,16 @@ public final class Launcher extends Application {
LOG.warning("Failed to get prism pipeline", e);
}
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
try {
try {
ConfigHolder.init();
} catch (SambaException e) {
showAlert(AlertType.WARNING, i18n("fatal.samba"));
} catch (SambaException ignored) {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (IOException e) {
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
EntryPoint.exit(1);
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
}
// https://lapcatsoftware.com/articles/app-translocation.html
@@ -113,8 +105,12 @@ public final class Launcher extends Application {
return;
}
if (ConfigHolder.isUnsupportedVersion()) {
showAlert(AlertType.WARNING, i18n("fatal.config_unsupported_version"));
}
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
}
// runLater to ensure ConfigHolder.init() finished initialization
@@ -177,7 +173,7 @@ public final class Launcher extends Application {
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
EntryPoint.exit(0);
Main.exit(0);
}
}
@@ -217,7 +213,7 @@ public final class Launcher extends Application {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
EntryPoint.exit(1);
Main.exit(1);
}
@Override
@@ -17,26 +17,41 @@
*/
package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.ModuleHelper;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.concurrent.CancellationException;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class EntryPoint {
public final class Main {
private EntryPoint() {
private Main() {
}
public static void main(String[] args) {
@@ -64,6 +79,10 @@ public final class EntryPoint {
checkDirectoryPath();
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
initIcon();
@@ -93,8 +112,6 @@ public final class EntryPoint {
}
}
} catch (IOException e) {
// Logger has not been started yet, so print directly to System.err
System.err.println("Failed to create HMCL directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
}
@@ -110,17 +127,16 @@ public final class EntryPoint {
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(EntryPoint.class.getResource("/assets/img/icon-mac.png"));
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon-mac.png"));
AwtUtils.setAppleIcon(image);
}
private static void checkDirectoryPath() {
String currentDir = System.getProperty("user.dir", "");
if (currentDir.contains("!")) {
LOG.error("The current working path contains an exclamation mark: " + currentDir);
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDir);
+ "The path is " + currentDirectory);
}
}
@@ -128,10 +144,10 @@ public final class EntryPoint {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.error("Unable to patch JVM", e);
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.error("Unable to patch JVM", e);
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.error("User cancels downloading JavaFX", e);
@@ -148,7 +164,7 @@ public final class EntryPoint {
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
LOG.warning("JavaFX is incomplete or not found", e);
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
@@ -156,20 +172,9 @@ public final class EntryPoint {
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
// javafx.graphics
Module module = Class.forName("javafx.stage.Stage").getModule();
if (module.isNamed()) {
try {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle implAddEnableNativeAccess = lookup.findVirtual(Module.class,
"implAddEnableNativeAccess", MethodType.methodType(Module.class));
Module ignored = (Module) implAddEnableNativeAccess.invokeExact(module);
} catch (Throwable e) {
e.printStackTrace(System.err);
}
}
ModuleHelper.addEnableNativeAccess(Class.forName("javafx.stage.Stage")); // javafx.graphics
} catch (ClassNotFoundException e) {
LOG.error("Failed to add enable native access for JavaFX", e);
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
@@ -180,11 +185,11 @@ public final class EntryPoint {
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
try {
Class<?> clazz = Class.forName("sun.misc.Unsafe");
boolean ignored = (boolean) MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
.findStatic(clazz, "trySetMemoryAccessWarned", MethodType.methodType(boolean.class))
.invokeExact();
Method trySetMemoryAccessWarned = clazz.getDeclaredMethod("trySetMemoryAccessWarned");
trySetMemoryAccessWarned.setAccessible(true);
trySetMemoryAccessWarned.invoke(null);
} catch (Throwable e) {
LOG.warning("Failed to enable unsafe memory access", e);
e.printStackTrace(System.err);
}
}
}
@@ -192,8 +197,70 @@ public final class EntryPoint {
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
private static void showErrorAndExit(String message) {
static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.ERROR, message).showAndWait();
exit(1);
}
} catch (Throwable ignored) {
}
SwingUtils.showErrorDialog(message);
exit(1);
}
/**
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
*/
static void showWarningAndContinue(String message) {
System.err.println(message);
System.err.println("Potential issues have been detected.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.WARNING, message).showAndWait();
return;
}
} catch (Throwable ignored) {
}
SwingUtils.showWarningDialog(message);
}
private static void fixLetsEncrypt() {
try {
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
try (InputStream ksStream = Files.newInputStream(ksPath)) {
defaultKeyStore.load(ksStream, "changeit".toCharArray());
}
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
}
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
merged.load(null, new char[0]);
for (String alias : Collections.list(letsEncryptKeyStore.aliases()))
merged.setCertificateEntry(alias, letsEncryptKeyStore.getCertificate(alias));
for (String alias : Collections.list(defaultKeyStore.aliases()))
merged.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
instance.init(merged);
SSLContext tls = SSLContext.getInstance("TLS");
tls.init(null, instance.getTrustManagers(), null);
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
LOG.info("Added Lets Encrypt root certificates as additional trust");
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
KeyManagementException e) {
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
}
}
}
@@ -19,10 +19,13 @@ package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
/**
* Stores metadata about this application.
@@ -35,6 +38,9 @@ public final class Metadata {
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 8;
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
@@ -94,4 +100,31 @@ public final class Metadata {
return !isStable() && !isDev();
}
public static @Nullable String getSuggestedJavaDownloadLink() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
return "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
else {
EnumSet<Architecture> supportedArchitectures;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.X86, Architecture.ARM64);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
supportedArchitectures = EnumSet.of(
Architecture.X86_64, Architecture.X86,
Architecture.ARM64, Architecture.ARM32,
Architecture.RISCV64, Architecture.LOONGARCH64
);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
else
supportedArchitectures = EnumSet.noneOf(Architecture.class);
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
return String.format("https://docs.hmcl.net/downloads/%s/%s.html",
OperatingSystem.CURRENT_OS.getCheckedName(),
Architecture.SYSTEM_ARCH.getCheckedName()
);
else
return null;
}
}
}
@@ -158,8 +158,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
blackList.add("saves");
if (Files.exists(dstDir)) throw new IOException("Version exists");
Files.createDirectories(dstDir);
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
Path fromJson = srcDir.resolve(srcId + ".json");
@@ -172,7 +170,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
Files.copy(fromJson, toJson);
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId));
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
@@ -196,7 +194,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File file = getLocalVersionSettingFile(id);
if (file.exists())
try {
VersionSetting versionSetting = GSON.fromJson(Files.readString(file.toPath()), VersionSetting.class);
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
initLocalVersionSetting(id, versionSetting);
} catch (Exception ex) {
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
@@ -429,7 +427,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File json = getModpackConfiguration(version);
if (json.exists()) {
try {
String jsonText = Files.readString(json.toPath());
String jsonText = FileUtils.readText(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
@@ -65,7 +66,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.fromJsonFile(json.toPath(), ModpackConfiguration.typeOf(Modpack.class));
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
@@ -53,7 +53,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.*;
@@ -269,27 +269,27 @@ public final class LauncherHelper {
if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
int responseCode = rce.getResponseCode();
String uri = rce.getUri();
URL url = rce.getUrl();
if (responseCode == 404)
message += i18n("download.code.404", uri);
message += i18n("download.code.404", url);
else
message += i18n("download.failed", uri, responseCode);
message += i18n("download.failed", url, responseCode);
} else {
message += StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof DownloadException) {
URI uri = ((DownloadException) ex).getUri();
URL url = ((DownloadException) ex).getUrl();
if (ex.getCause() instanceof SocketTimeoutException) {
message = i18n("install.failed.downloading.timeout", uri);
message = i18n("install.failed.downloading.timeout", url);
} else if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else {
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
message = i18n("assets.index.malformed");
@@ -300,11 +300,11 @@ public final class LauncherHelper {
} else if (ex instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex;
int responseCode = rce.getResponseCode();
String uri = rce.getUri();
URL url = rce.getUrl();
if (responseCode == 404)
message = i18n("download.code.404", uri);
message = i18n("download.code.404", url);
else
message = i18n("download.failed", uri, responseCode);
message = i18n("download.failed", url, responseCode);
} else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
@@ -42,9 +41,9 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
protected abstract SortType getBackedRemoteModRepositorySortOrder();
@Override
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
if (!StringUtils.containsChinese(searchFilter)) {
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
}
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
@@ -66,7 +65,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
{
SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
if (chineseIndex > englishIndex) {
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
@@ -140,7 +140,7 @@ public final class ModpackHelper {
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.fromJsonFile(file.toPath(), ModpackConfiguration.class);
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -103,6 +104,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
@@ -45,6 +45,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -109,7 +110,7 @@ public final class TexturesLoader {
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(texture.getUrl(), file).run();
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
@@ -115,7 +116,11 @@ public final class HMCLJavaRepository implements JavaRepository {
}
if (Files.isDirectory(javaDir)) {
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
JavaManifest manifest;
try (InputStream input = Files.newInputStream(file)) {
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
}
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
}
}
@@ -186,7 +191,7 @@ public final class HMCLJavaRepository implements JavaRepository {
});
JavaManifest manifest = new JavaManifest(info, update, files);
JsonUtils.writeToJsonFile(getManifestFile(platform, gameJavaVersion), manifest);
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
return JavaRuntime.of(executable, info, true);
});
}
@@ -198,7 +203,7 @@ public final class HMCLJavaRepository implements JavaRepository {
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
return JavaRuntime.of(executable, result.getInfo(), true);
});
}
@@ -255,16 +255,15 @@ public final class JavaManager {
}
}
private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) {
if (suggestedJavaVersion != null) {
boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
private static JavaRuntime chooseJava(@Nullable JavaRuntime java1, JavaRuntime java2) {
if (java1 == null)
return java2;
if (b1 != b2)
return b1 ? 1 : -1;
}
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
if (java1.getParsedVersion() != java2.getParsedVersion())
// Prefer the Java version that is closer to the game's recommended Java version
return java1.getParsedVersion() < java2.getParsedVersion() ? java1 : java2;
else
return java1.getVersionNumber().compareTo(java2.getVersionNumber()) >= 0 ? java1 : java2;
}
@Nullable
@@ -280,9 +279,6 @@ public final class JavaManager {
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
GameJavaVersion suggestedJavaVersion =
(version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null;
JavaRuntime mandatory = null;
JavaRuntime suggested = null;
for (JavaRuntime java : javaRuntimes) {
@@ -310,15 +306,10 @@ public final class JavaManager {
}
if (!violationMandatory) {
if (mandatory == null) mandatory = java;
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
mandatory = java;
mandatory = chooseJava(mandatory, java);
if (!violationSuggested) {
if (suggested == null) suggested = java;
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
suggested = java;
}
if (!violationSuggested)
suggested = chooseJava(suggested, java);
}
}
@@ -412,15 +403,8 @@ public final class JavaManager {
if (System.getenv("PATH") != null) {
String[] paths = System.getenv("PATH").split(File.pathSeparator);
for (String path : paths) {
// https://github.com/HMCL-dev/HMCL/issues/4079
// https://github.com/Meloong-Git/PCL/issues/4261
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && path.toLowerCase(Locale.ROOT)
.contains("\\common files\\oracle\\java\\")) {
continue;
}
try {
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
@@ -488,15 +472,12 @@ public final class JavaManager {
try {
info = JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException e) {
LOG.warning("Failed to read release file " + releaseFile, e);
}
}
if (info == null) {
try {
info = JavaInfoUtils.fromExecutable(executable, false);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
try {
info = JavaInfoUtils.fromExecutable(executable, false);
} catch (IOException e2) {
e2.addSuppressed(e);
LOG.warning("Failed to lookup Java executable at " + executable, e2);
}
}
}
@@ -36,7 +36,6 @@ import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import javax.net.ssl.SSLException;
@@ -69,6 +68,18 @@ public final class Accounts {
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
private static void triggerAuthlibInjectorUpdateCheck() {
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
Schedulers.io().execute(() -> {
try {
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
} catch (IOException e) {
LOG.warning("Failed to check update for authlib-injector", e);
}
});
}
}
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
@@ -326,6 +337,8 @@ public final class Accounts {
});
}
triggerAuthlibInjectorUpdateCheck();
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
@@ -333,7 +346,7 @@ public final class Accounts {
try {
server.fetchMetadataResponse();
} catch (IOException e) {
LOG.warning("Failed to fetch authlib-injector server metadata: " + server, e);
LOG.warning("Failed to fetch authlib-injector server metdata: " + server, e);
}
});
}
@@ -358,18 +371,24 @@ public final class Accounts {
// ==== authlib-injector ====
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
if (authlibinjectorLocation != null) {
if (authlibinjectorLocation == null) {
return new AuthlibInjectorDownloader(
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve("authlib-injector.jar"),
DownloadProviders::getDownloadProvider) {
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
if (local.isPresent()) {
return local;
}
// search authlib-injector.jar in current directory, it's used as a fallback
return parseArtifact(Metadata.CURRENT_DIRECTORY.resolve("authlib-injector.jar"));
}
};
} else {
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
}
String authlibInjectorVersion = JarUtils.getManifestAttribute("Authlib-Injector-Version", null);
if (authlibInjectorVersion == null)
throw new AssertionError("Missing Authlib-Injector-Version");
String authlibInjectorFileName = "authlib-injector-" + authlibInjectorVersion + ".jar";
return new AuthlibInjectorExtractor(Accounts.class.getResource("/assets/" + authlibInjectorFileName),
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve(authlibInjectorFileName));
}
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import java.io.IOException;
@@ -72,7 +73,8 @@ public final class AuthlibInjectorServers implements Validation {
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
AuthlibInjectorServers configInstance;
try {
configInstance = JsonUtils.fromJsonFile(configLocation, AuthlibInjectorServers.class);
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.warning("Malformed authlib-injectors.json", e);
return;
@@ -29,7 +29,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.scene.paint.Paint;
import org.hildan.fxgson.creators.ObservableListCreator;
import org.hildan.fxgson.creators.ObservableMapCreator;
import org.hildan.fxgson.creators.ObservableSetCreator;
@@ -38,7 +37,6 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import org.jackhuang.hmcl.util.gson.PaintAdapter;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
@@ -52,6 +50,7 @@ import java.util.TreeMap;
public final class Config implements Observable {
public static final int CURRENT_VERSION = 2;
public static final int CURRENT_UI_VERSION = 0;
public static final Gson CONFIG_GSON = new GsonBuilder()
@@ -62,7 +61,6 @@ public final class Config implements Observable {
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.registerTypeAdapter(Paint.class, new PaintAdapter())
.setPrettyPrinting()
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
.create();
@@ -90,12 +88,6 @@ public final class Config implements Observable {
@SerializedName("bgurl")
private StringProperty backgroundImageUrl = new SimpleStringProperty();
@SerializedName("bgImageOpacity")
private IntegerProperty backgroundImageOpacity = new SimpleIntegerProperty(100);
@SerializedName("bgpaint")
private ObjectProperty<Paint> backgroundPaint = new SimpleObjectProperty<>();
@SerializedName("commonDirType")
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
@@ -181,7 +173,7 @@ public final class Config implements Observable {
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[]{server});
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("addedLittleSkin")
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
@@ -189,6 +181,9 @@ public final class Config implements Observable {
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("acceptPreviewUpdate")
private BooleanProperty acceptPreviewUpdate = new SimpleBooleanProperty(false);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -283,30 +278,6 @@ public final class Config implements Observable {
this.backgroundImageUrl.set(backgroundImageUrl);
}
public Paint getBackgroundPaint() {
return backgroundPaint.get();
}
public ObjectProperty<Paint> backgroundPaintProperty() {
return backgroundPaint;
}
public void setBackgroundPaint(Paint backgroundPaint) {
this.backgroundPaint.set(backgroundPaint);
}
public int getBackgroundImageOpacity() {
return backgroundImageOpacity.get();
}
public void setBackgroundImageOpacity(int backgroundImageOpacity) {
this.backgroundImageOpacity.set(backgroundImageOpacity);
}
public IntegerProperty backgroundImageOpacityProperty() {
return backgroundImageOpacity;
}
public EnumCommonDirectory getCommonDirType() {
return commonDirType.get();
}
@@ -703,6 +674,18 @@ public final class Config implements Observable {
this.promptedVersion.set(promptedVersion);
}
public boolean isAcceptPreviewUpdate() {
return acceptPreviewUpdate.get();
}
public BooleanProperty acceptPreviewUpdateProperty() {
return acceptPreviewUpdate;
}
public void setAcceptPreviewUpdate(boolean acceptPreviewUpdate) {
this.acceptPreviewUpdate.set(acceptPreviewUpdate);
}
public ObservableMap<String, Object> getShownTips() {
return shownTips;
}
@@ -45,6 +45,7 @@ public final class ConfigHolder {
private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated;
private static boolean ownerChanged = false;
private static boolean unsupportedVersion = false;
public static Config config() {
if (configInstance == null) {
@@ -72,6 +73,10 @@ public final class ConfigHolder {
return ownerChanged;
}
public static boolean isUnsupportedVersion() {
return unsupportedVersion;
}
public static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
@@ -82,7 +87,8 @@ public final class ConfigHolder {
LOG.info("Config location: " + configLocation);
configInstance = loadConfig();
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
if (!unsupportedVersion)
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
globalConfigInstance = loadGlobalConfig();
globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson()));
@@ -101,7 +107,6 @@ public final class ConfigHolder {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& configLocation.getFileSystem() == FileSystems.getDefault()
&& configLocation.toFile().canWrite()) {
LOG.warning("Config at " + configLocation + " is not writable, but it seems to be a Samba share or OpenJDK bug");
// There are some serious problems with the implementation of Samba or OpenJDK
throw new SambaException();
} else {
@@ -158,12 +163,19 @@ public final class ConfigHolder {
LOG.warning("Failed to get owner");
}
try {
String content = Files.readString(configLocation);
String content = FileUtils.readText(configLocation);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
ConfigUpgrader.upgradeConfig(deserialized, content);
int configVersion = deserialized.getConfigVersion();
if (configVersion < Config.CURRENT_VERSION) {
ConfigUpgrader.upgradeConfig(deserialized, content);
} else if (configVersion > Config.CURRENT_VERSION) {
unsupportedVersion = true;
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", Config.CURRENT_VERSION, configVersion));
}
return deserialized;
}
} catch (JsonParseException e) {
@@ -180,7 +192,7 @@ public final class ConfigHolder {
private static GlobalConfig loadGlobalConfig() throws IOException {
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = Files.readString(GLOBAL_CONFIG_PATH);
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
GlobalConfig deserialized = GlobalConfig.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
@@ -31,8 +31,6 @@ final class ConfigUpgrader {
private ConfigUpgrader() {
}
private static final int CURRENT_VERSION = 2;
/**
* This method is for the compatibility with old HMCL versions.
*
@@ -42,17 +40,10 @@ final class ConfigUpgrader {
static void upgradeConfig(Config deserialized, String rawContent) {
int configVersion = deserialized.getConfigVersion();
if (configVersion == CURRENT_VERSION) {
if (configVersion >= Config.CURRENT_VERSION)
return;
}
if (configVersion > CURRENT_VERSION) {
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
deserialized.setConfigVersion(CURRENT_VERSION);
return;
}
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, Config.CURRENT_VERSION));
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
if (configVersion < 1) {
@@ -100,6 +91,6 @@ final class ConfigUpgrader {
}
}
deserialized.setConfigVersion(CURRENT_VERSION);
deserialized.setConfigVersion(Config.CURRENT_VERSION);
}
}
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.Arrays;
import java.util.Map;
@@ -137,26 +137,26 @@ public final class DownloadProviders {
public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) {
URI uri = ((DownloadException) exception).getUri();
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
return i18n("install.failed.downloading.timeout", uri);
return i18n("install.failed.downloading.timeout", url);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
return i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception.getCause() instanceof FileNotFoundException) {
return i18n("download.code.404", uri);
return i18n("download.code.404", url);
} else if (exception.getCause() instanceof AccessDeniedException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
} else {
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception instanceof ArtifactMalformedException) {
return i18n("exception.artifact_malformed");
@@ -22,6 +22,5 @@ public enum EnumBackgroundImage {
CUSTOM,
CLASSIC,
NETWORK,
TRANSLUCENT, // Deprecated
PAINT
TRANSLUCENT
}
@@ -21,6 +21,7 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -28,6 +29,9 @@ import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -148,33 +152,46 @@ public final class FontManager {
return null;
}
Font[] fonts = Font.loadFonts(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (fonts == null) {
LOG.warning("Failed to load font from " + path);
return null;
} else if (fonts.length == 0) {
LOG.warning("No fonts loaded from " + path);
return null;
}
if (JavaRuntime.CURRENT_VERSION >= 9) {
try {
MethodHandle methodHandle = MethodHandles.publicLookup().findStatic(Font.class, "loadFonts",
MethodType.methodType(Font[].class, String.class, double.class));
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
Font[] fonts = (Font[]) methodHandle.invokeExact(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (fonts == null) {
LOG.warning("Failed to load font from " + path);
return null;
} else if (fonts.length == 0) {
LOG.warning("No fonts loaded from " + path);
return null;
}
if (family.indexOf(',') >= 0) {
for (String candidateFamily : family.split(",")) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
if (family.indexOf(',') >= 0) {
for (String candidateFamily : family.split(",")) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
return font;
}
}
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
} catch (NoSuchMethodException | IllegalAccessException ignored) {
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
Font font = Font.loadFont(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (font == null)
LOG.warning("Failed to load font from " + path);
return font;
} catch (Throwable e) {
LOG.warning("Failed to get default font with fc-match", e);
return null;
@@ -21,7 +21,10 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
@@ -54,8 +57,6 @@ public final class GlobalConfig implements Observable {
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
@@ -130,18 +131,6 @@ public final class GlobalConfig implements Observable {
enableOfflineAccount.set(value);
}
public StringProperty fontAntiAliasingProperty() {
return fontAntiAliasing;
}
public String getFontAntiAliasing() {
return fontAntiAliasing.get();
}
public void setFontAntiAliasing(String value) {
this.fontAntiAliasing.set(value);
}
public ObservableSet<String> getUserJava() {
return userJava;
}
@@ -157,8 +146,7 @@ public final class GlobalConfig implements Observable {
"logRetention",
"userJava",
"disabledJava",
"enableOfflineAccount",
"fontAntiAliasing"
"enableOfflineAccount"
));
@Override
@@ -171,7 +159,6 @@ public final class GlobalConfig implements Observable {
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
if (src.enableOfflineAccount.get())
jsonObject.addProperty("enableOfflineAccount", true);
@@ -199,7 +186,6 @@ public final class GlobalConfig implements Observable {
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
config.setFontAntiAliasing(Optional.ofNullable(obj.get("fontAntiAliasing")).map(JsonElement::getAsString).orElse(null));
JsonElement userJava = obj.get("userJava");
if (userJava != null && userJava.isJsonArray()) {
@@ -18,15 +18,13 @@
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.*;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -34,10 +32,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ProxyManager {
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT = Objects.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
private static final ProxySelector SYSTEM_DEFAULT = Lang.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static ProxySelector getProxySelector() {
if (config().hasProxy()) {
@@ -58,13 +53,16 @@ public final class ProxyManager {
}
}
private static SimpleAuthenticator getAuthenticator() {
private static Authenticator getAuthenticator() {
if (config().hasProxy() && config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
if (username != null || password != null)
return new SimpleAuthenticator(username, password.toCharArray());
return new SimpleAuthenticator(
Lang.requireNonNullElse(username, ""),
Lang.requireNonNullElse(password, "").toCharArray()
);
else
return null;
} else
@@ -72,34 +70,15 @@ public final class ProxyManager {
}
static void init() {
ProxySelector.setDefault(new ProxySelector() {
@Override
public List<Proxy> select(URI uri) {
return defaultProxySelector.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
defaultProxySelector.connectFailed(uri, sa, ioe);
}
});
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
var defaultAuthenticator = ProxyManager.defaultAuthenticator;
return defaultAuthenticator != null ? defaultAuthenticator.getPasswordAuthentication() : null;
}
});
defaultProxySelector = getProxySelector();
InvalidationListener updateProxySelector = observable -> defaultProxySelector = getProxySelector();
ProxySelector.setDefault(getProxySelector());
InvalidationListener updateProxySelector = observable -> ProxySelector.setDefault(getProxySelector());
config().proxyTypeProperty().addListener(updateProxySelector);
config().proxyHostProperty().addListener(updateProxySelector);
config().proxyPortProperty().addListener(updateProxySelector);
config().hasProxyProperty().addListener(updateProxySelector);
defaultAuthenticator = getAuthenticator();
InvalidationListener updateAuthenticator = observable -> defaultAuthenticator = getAuthenticator();
Authenticator.setDefault(getAuthenticator());
InvalidationListener updateAuthenticator = observable -> Authenticator.setDefault(getAuthenticator());
config().hasProxyProperty().addListener(updateAuthenticator);
config().hasProxyAuthProperty().addListener(updateAuthenticator);
config().proxyUserProperty().addListener(updateAuthenticator);
@@ -148,7 +127,7 @@ public final class ProxyManager {
}
@Override
public PasswordAuthentication getPasswordAuthentication() {
protected PasswordAuthentication getPasswordAuthentication() {
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
}
}
@@ -23,6 +23,7 @@ import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -67,7 +68,7 @@ public final class StyleSheets {
Path temp = Files.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
Files.writeString(temp, styleSheet, Charset.defaultCharset());
FileUtils.writeText(temp, styleSheet, Charset.defaultCharset());
temp.toFile().deleteOnExit();
return temp.toUri().toString();
} catch (IOException | NullPointerException e) {
@@ -29,7 +29,6 @@ public enum VersionIconType {
COMMAND("/assets/img/command.png"),
OPTIFINE("/assets/img/optifine.png"),
CRAFT_TABLE("/assets/img/craft_table.png"),
APRIL_FOOLS("/assets/img/april_fools.png"),
FABRIC("/assets/img/fabric.png"),
FORGE("/assets/img/forge.png"),
NEO_FORGE("/assets/img/neoforge.png"),
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -175,18 +176,6 @@ public final class Controllers {
public static void initialize(Stage stage) {
LOG.info("Start initializing application");
if (System.getProperty("prism.lcdtext") == null) {
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
LOG.info("Enable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "true");
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
LOG.info("Disable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "false");
}
}
Controllers.stage = stage;
stageSizeChangeListener = o -> {
@@ -216,9 +205,7 @@ public final class Controllers {
if (targetProperty != null
&& Controllers.stage != null
&& !Controllers.stage.isIconified()
&& !Controllers.stage.isFullScreen()
&& !Controllers.stage.isMaximized()) {
&& !Controllers.stage.isIconified()) {
targetProperty.set(sourceProperty.get());
}
};
@@ -323,6 +310,26 @@ public final class Controllers {
}
}
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
Number shownTipVersion = null;
try {
shownTipVersion = (Number) config().getShownTips().get(JAVA_VERSION_TIP);
} catch (ClassCastException e) {
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
}
if (shownTipVersion == null || shownTipVersion.intValue() < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
if (downloadLink != null)
builder.addHyperLink(i18n("fatal.deprecated_java_version.download_link", 21), downloadLink);
Controllers.dialog(builder
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, Metadata.MINIMUM_SUPPORTED_JAVA_VERSION))
.build());
}
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
@@ -18,16 +18,17 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.WeakListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.collections.ObservableMap;
import javafx.event.Event;
import javafx.event.EventDispatcher;
import javafx.event.EventType;
@@ -41,9 +42,11 @@ import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
@@ -52,12 +55,10 @@ import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.task.CacheFileTask;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.image.ImageLoader;
import org.jackhuang.hmcl.ui.image.ImageUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -65,23 +66,28 @@ import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.ref.WeakReference;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -104,11 +110,6 @@ public final class FXUtils {
public static final int JAVAFX_MAJOR_VERSION;
/// @see Platform.Preferences
public static final @Nullable ObservableMap<String, Object> PREFERENCES;
public static final @Nullable ObservableBooleanValue DARK_MODE;
public static final @Nullable Boolean REDUCED_MOTION;
static {
String jfxVersion = System.getProperty("javafx.version");
int majorVersion = -1;
@@ -119,51 +120,27 @@ public final class FXUtils {
}
}
JAVAFX_MAJOR_VERSION = majorVersion;
ObservableMap<String, Object> preferences = null;
ObservableBooleanValue darkMode = null;
Boolean reducedMotion = null;
if (JAVAFX_MAJOR_VERSION >= 22) {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
Class<?> preferencesClass = Class.forName("javafx.application.Platform$Preferences");
@SuppressWarnings("unchecked")
var preferences0 = (ObservableMap<String, Object>) lookup.findStatic(Platform.class, "getPreferences", MethodType.methodType(preferencesClass))
.invoke();
preferences = preferences0;
@SuppressWarnings("unchecked")
var colorSchemeProperty =
(ReadOnlyObjectProperty<? extends Enum<?>>)
lookup.findVirtual(preferencesClass, "colorSchemeProperty", MethodType.methodType(ReadOnlyObjectProperty.class))
.invoke(preferences);
darkMode = Bindings.createBooleanBinding(() ->
"DARK".equals(colorSchemeProperty.get().name()), colorSchemeProperty);
if (JAVAFX_MAJOR_VERSION >= 24) {
reducedMotion = (boolean)
lookup.findVirtual(preferencesClass, "isReducedMotion", MethodType.methodType(boolean.class))
.invoke(preferences);
}
} catch (Throwable e) {
LOG.warning("Failed to get preferences", e);
}
}
PREFERENCES = preferences;
DARK_MODE = darkMode;
REDUCED_MOTION = reducedMotion;
}
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "apng"
"png", "jpg", "jpeg", "bmp", "gif", "webp"
);
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
public static void shutdown() {
for (Map.Entry<String, Path> entry : remoteImageCache.entrySet()) {
try {
Files.deleteIfExists(entry.getValue());
} catch (IOException e) {
LOG.warning(String.format("Failed to delete cache file %s.", entry.getValue()), e);
}
remoteImageCache.remove(entry.getKey());
}
builtinImageCache.clear();
}
@@ -356,15 +333,8 @@ public final class FXUtils {
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
public static void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
tooltip.setShowDelay(showDelay);
tooltip.setShowDuration(showDuration);
tooltip.setHideDelay(hideDelay);
Tooltip.install(node, tooltip);
}
public static void installFastTooltip(Node node, Tooltip tooltip) {
runInFX(() -> installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installFastTooltip(Node node, String tooltip) {
@@ -372,7 +342,7 @@ public final class FXUtils {
}
public static void installSlowTooltip(Node node, Tooltip tooltip) {
runInFX(() -> installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installSlowTooltip(Node node, String tooltip) {
@@ -735,240 +705,6 @@ public final class FXUtils {
property.removeListener(binding);
}
private static final class PaintBidirectionalBinding implements InvalidationListener, WeakListener {
private final WeakReference<ColorPicker> colorPickerRef;
private final WeakReference<Property<Paint>> propertyRef;
private final int hashCode;
private boolean updating = false;
private PaintBidirectionalBinding(ColorPicker colorPicker, Property<Paint> property) {
this.colorPickerRef = new WeakReference<>(colorPicker);
this.propertyRef = new WeakReference<>(property);
this.hashCode = System.identityHashCode(colorPicker) ^ System.identityHashCode(property);
}
@Override
public void invalidated(Observable sourceProperty) {
if (!updating) {
final ColorPicker colorPicker = colorPickerRef.get();
final Property<Paint> property = propertyRef.get();
if (colorPicker == null || property == null) {
if (colorPicker != null) {
colorPicker.valueProperty().removeListener(this);
}
if (property != null) {
property.removeListener(this);
}
} else {
updating = true;
try {
if (property == sourceProperty) {
Paint newValue = property.getValue();
if (newValue instanceof Color)
colorPicker.setValue((Color) newValue);
else
colorPicker.setValue(null);
} else {
Paint newValue = colorPicker.getValue();
property.setValue(newValue);
}
} finally {
updating = false;
}
}
}
}
@Override
public boolean wasGarbageCollected() {
return colorPickerRef.get() == null || propertyRef.get() == null;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof FXUtils.PaintBidirectionalBinding))
return false;
var that = (FXUtils.PaintBidirectionalBinding) o;
final ColorPicker colorPicker = this.colorPickerRef.get();
final Property<Paint> property = this.propertyRef.get();
final ColorPicker thatColorPicker = that.colorPickerRef.get();
final Property<?> thatProperty = that.propertyRef.get();
if (colorPicker == null || property == null || thatColorPicker == null || thatProperty == null)
return false;
return colorPicker == thatColorPicker && property == thatProperty;
}
}
public static void bindPaint(ColorPicker colorPicker, Property<Paint> property) {
PaintBidirectionalBinding binding = new PaintBidirectionalBinding(colorPicker, property);
colorPicker.valueProperty().removeListener(binding);
property.removeListener(binding);
if (property.getValue() instanceof Color)
colorPicker.setValue((Color) property.getValue());
else
colorPicker.setValue(null);
colorPicker.valueProperty().addListener(binding);
property.addListener(binding);
}
private static final class WindowsSizeBidirectionalBinding implements InvalidationListener, WeakListener {
private final WeakReference<JFXComboBox<String>> comboBoxRef;
private final WeakReference<IntegerProperty> widthPropertyRef;
private final WeakReference<IntegerProperty> heightPropertyRef;
private final int hashCode;
private boolean updating = false;
private WindowsSizeBidirectionalBinding(JFXComboBox<String> comboBox,
IntegerProperty widthProperty,
IntegerProperty heightProperty) {
this.comboBoxRef = new WeakReference<>(comboBox);
this.widthPropertyRef = new WeakReference<>(widthProperty);
this.heightPropertyRef = new WeakReference<>(heightProperty);
this.hashCode = System.identityHashCode(comboBox)
^ System.identityHashCode(widthProperty)
^ System.identityHashCode(heightProperty);
}
@Override
public void invalidated(Observable observable) {
if (!updating) {
var comboBox = this.comboBoxRef.get();
var widthProperty = this.widthPropertyRef.get();
var heightProperty = this.heightPropertyRef.get();
if (comboBox == null || widthProperty == null || heightProperty == null) {
if (comboBox != null) {
comboBox.focusedProperty().removeListener(this);
comboBox.sceneProperty().removeListener(this);
}
if (widthProperty != null)
widthProperty.removeListener(this);
if (heightProperty != null)
heightProperty.removeListener(this);
} else {
updating = true;
try {
int width = widthProperty.get();
int height = heightProperty.get();
if (observable instanceof ReadOnlyProperty<?>
&& ((ReadOnlyProperty<?>) observable).getBean() == comboBox) {
String value = comboBox.valueProperty().get();
if (value == null)
value = "";
int idx = value.indexOf('x');
if (idx < 0)
idx = value.indexOf('*');
if (idx < 0) {
LOG.warning("Bad window size: " + value);
comboBox.setValue(width + "x" + height);
return;
}
String widthStr = value.substring(0, idx).trim();
String heightStr = value.substring(idx + 1).trim();
int newWidth;
int newHeight;
try {
newWidth = Integer.parseInt(widthStr);
newHeight = Integer.parseInt(heightStr);
} catch (NumberFormatException e) {
LOG.warning("Bad window size: " + value);
comboBox.setValue(width + "x" + height);
return;
}
widthProperty.set(newWidth);
heightProperty.set(newHeight);
} else {
comboBox.setValue(width + "x" + height);
}
} finally {
updating = false;
}
}
}
}
@Override
public boolean wasGarbageCollected() {
return this.comboBoxRef.get() == null
|| this.widthPropertyRef.get() == null
|| this.heightPropertyRef.get() == null;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof WindowsSizeBidirectionalBinding))
return false;
var that = (WindowsSizeBidirectionalBinding) obj;
var comboBox = this.comboBoxRef.get();
var widthProperty = this.widthPropertyRef.get();
var heightProperty = this.heightPropertyRef.get();
var thatComboBox = that.comboBoxRef.get();
var thatWidthProperty = that.widthPropertyRef.get();
var thatHeightProperty = that.heightPropertyRef.get();
if (comboBox == null || widthProperty == null || heightProperty == null
|| thatComboBox == null || thatWidthProperty == null || thatHeightProperty == null) {
return false;
}
return comboBox == thatComboBox
&& widthProperty == thatWidthProperty
&& heightProperty == thatHeightProperty;
}
}
public static void bindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
comboBox.setValue(widthProperty.get() + "x" + heightProperty.get());
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
comboBox.focusedProperty().addListener(binding);
comboBox.sceneProperty().addListener(binding);
widthProperty.addListener(binding);
heightProperty.addListener(binding);
}
public static void unbindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
comboBox.focusedProperty().removeListener(binding);
comboBox.sceneProperty().removeListener(binding);
widthProperty.removeListener(binding);
heightProperty.removeListener(binding);
}
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
int itemCount = children.length;
int childSelectedCount = 0;
@@ -1036,53 +772,98 @@ public final class FXUtils {
stage.getIcons().add(newBuiltinImage(icon));
}
private static Image loadWebPImage(InputStream input) throws IOException {
WebPImageReaderSpi spi = new WebPImageReaderSpi();
ImageReader reader = spi.createReaderInstance(null);
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
reader.setInput(imageInput, true, true);
return SwingFXUtils.toFXImage(reader.read(0, reader.getDefaultReadParam()), null);
} finally {
reader.dispose();
}
}
public static Image loadWebPImage(InputStream input,
int requestedWidth, int requestedHeight,
boolean preserveRatio, boolean smooth) throws IOException {
WebPImageReaderSpi spi = new WebPImageReaderSpi();
ImageReader reader = spi.createReaderInstance(null);
BufferedImage bufferedImage;
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
reader.setInput(imageInput, true, true);
bufferedImage = reader.read(0, reader.getDefaultReadParam());
} finally {
reader.dispose();
}
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
}
public static Image loadImage(Path path) throws Exception {
return loadImage(path, 0, 0, false, false);
try (InputStream input = Files.newInputStream(path)) {
if ("webp".equalsIgnoreCase(FileUtils.getExtension(path)))
return loadWebPImage(input);
else {
Image image = new Image(input);
if (image.isError())
throw image.getException();
return image;
}
}
}
public static Image loadImage(Path path,
int requestedWidth, int requestedHeight,
boolean preserveRatio, boolean smooth) throws Exception {
try (var input = new BufferedInputStream(Files.newInputStream(path))) {
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
String ext = FileUtils.getExtension(path).toLowerCase(Locale.ROOT);
ImageLoader loader = ImageUtils.EXT_TO_LOADER.get(ext);
if (loader == null && !ImageUtils.DEFAULT_EXTS.contains(ext)) {
input.mark(ImageUtils.HEADER_BUFFER_SIZE);
byte[] headerBuffer = input.readNBytes(ImageUtils.HEADER_BUFFER_SIZE);
input.reset();
loader = ImageUtils.guessLoader(headerBuffer);
if ("webp".equalsIgnoreCase(ext))
return loadWebPImage(Channels.newInputStream(channel),
requestedWidth, requestedHeight, preserveRatio, smooth);
if (!IMAGE_EXTENSIONS.contains(ext)) {
byte[] header = new byte[12];
ByteBuffer buffer = ByteBuffer.wrap(header);
//noinspection StatementWithEmptyBody
while (buffer.hasRemaining() && channel.read(buffer) > 0) {
}
channel.position(0L);
if (!buffer.hasRemaining()) {
// WebP File
if (header[0] == 'R' && header[1] == 'I' && header[2] == 'F' && header[3] == 'F' &&
header[8] == 'W' && header[9] == 'E' && header[10] == 'B' && header[11] == 'P') {
return loadWebPImage(Channels.newInputStream(channel),
requestedWidth, requestedHeight, preserveRatio, smooth);
}
}
}
if (loader == null)
loader = ImageUtils.DEFAULT;
return loader.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
Image image = new Image(Channels.newInputStream(channel),
requestedWidth, requestedHeight,
preserveRatio, smooth);
if (image.isError())
throw image.getException();
return image;
}
}
public static Image loadImage(String url) throws Exception {
URI uri = NetworkUtils.toURI(url);
URLConnection connection = NetworkUtils.createConnection(uri);
if (connection instanceof HttpURLConnection)
public static Image loadImage(URL url) throws Exception {
URLConnection connection = NetworkUtils.createConnection(url);
if (connection instanceof HttpURLConnection) {
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
}
try (BufferedInputStream input = new BufferedInputStream(connection.getInputStream())) {
String contentType = Objects.requireNonNull(connection.getContentType(), "");
Matcher matcher = ImageUtils.CONTENT_TYPE_PATTERN.matcher(contentType);
if (matcher.find())
contentType = matcher.group("type");
ImageLoader loader = ImageUtils.CONTENT_TYPE_TO_LOADER.get(contentType);
if (loader == null && !ImageUtils.DEFAULT_CONTENT_TYPES.contains(contentType)) {
input.mark(ImageUtils.HEADER_BUFFER_SIZE);
byte[] headerBuffer = input.readNBytes(ImageUtils.HEADER_BUFFER_SIZE);
input.reset();
loader = ImageUtils.guessLoader(headerBuffer);
try (InputStream input = connection.getInputStream()) {
String path = url.getPath();
if (path != null && "webp".equalsIgnoreCase(StringUtils.substringAfterLast(path, '.')))
return loadWebPImage(input);
else {
Image image = new Image(input);
if (image.isError())
throw image.getException();
return image;
}
if (loader == null)
loader = ImageUtils.DEFAULT;
return loader.load(input, 0, 0, false, false);
}
}
@@ -1127,12 +908,55 @@ public final class FXUtils {
}
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(url)
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth));
return Task.composeAsync(() -> {
Path currentPath = remoteImageCache.get(url);
if (currentPath != null) {
if (Files.isReadable(currentPath))
return Task.completed(currentPath);
// The file is unavailable or unreadable.
remoteImageCache.remove(url);
try {
Files.deleteIfExists(currentPath);
} catch (IOException e) {
LOG.warning("An exception encountered while deleting broken cached image file.", e);
}
}
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
return new FileDownloadTask(NetworkUtils.toURL(url), newPath.toFile())
.thenSupplyAsync(() -> {
Path otherPath = remoteImageCache.putIfAbsent(url, newPath);
if (otherPath == null)
return newPath;
else {
// The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
Files.delete(newPath);
return otherPath;
}
});
}).thenApplyAsync(path -> loadImage(path, requestedWidth, requestedHeight, preserveRatio, smooth));
}
/**
* Load image from the internet. It will cache the data of images for the further usage.
* The cached data will be deleted when HMCL is closed or hidden.
*
* @param url the url of image. The image resource should be a file on the internet.
* @param requestedWidth the image's bounding box width
* @param requestedHeight the image's bounding box height
* @param preserveRatio indicates whether to preserve the aspect ratio of
* the original image when scaling to fit the image within the
* specified bounding box
* @param smooth indicates whether to use a better quality filtering
* algorithm or a faster one when scaling this image to fit within
* the specified bounding box
* @return the image resource within the jar.
*/
public static ObservableValue<Image> newRemoteImage(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
var image = new SimpleObjectProperty<Image>();
SimpleObjectProperty<Image> image = new SimpleObjectProperty<>();
getRemoteImageTask(url, requestedWidth, requestedHeight, preserveRatio, smooth)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
@@ -22,6 +22,7 @@ import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -312,7 +313,9 @@ public class InstallerItem extends Control {
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
private static final WeakListenerHolder holder = new WeakListenerHolder();
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final ChangeListener<Number> holder;
InstallerItemSkin(InstallerItem control) {
super(control);
@@ -320,9 +323,14 @@ public class InstallerItem extends Control {
Pane pane;
if (control.style == Style.CARD) {
pane = new VBox();
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
if (FXUtils.JAVAFX_MAJOR_VERSION == 8)
holder = FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
else
holder = FXUtils.onWeakChangeAndOperate(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
} else {
pane = new HBox();
holder = null;
}
pane.getStyleClass().add("installer-item");
RipplerContainer container = new RipplerContainer(pane);
@@ -38,6 +38,7 @@ import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
@@ -185,7 +186,7 @@ public final class LogWindow extends Stage {
}
private void onTerminateGame() {
LogWindow.this.gameProcess.stop();
LauncherHelper.stopManagedProcesses();
}
private void onClear() {
@@ -223,7 +224,7 @@ public final class LogWindow extends Stage {
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getProcess().pid(), dumpFile);
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
} catch (Throwable e) {
@@ -1,27 +1,7 @@
/*
* 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.util;
package org.jackhuang.hmcl.ui;
import javax.swing.*;
/**
* @author Glavo
*/
public final class SwingUtils {
static {
if (System.getProperty("swing.defaultlaf") == null) {
@@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public class TooltipInstaller {
public static final TooltipInstaller INSTALLER;
static {
TooltipInstaller installer = null;
try {
installer = new NewInstaller();
} catch (Throwable e) {
try {
installer = new OldInstaller();
} catch (Throwable e2) {
e2.addSuppressed(e);
LOG.warning("Failed to initialize TooltipInstaller", e2);
}
}
INSTALLER = installer != null ? installer : new TooltipInstaller();
}
TooltipInstaller() {
}
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
Tooltip.install(node, tooltip);
}
// For Java 8
private static final class OldInstaller extends TooltipInstaller {
private static final Constructor<?> createTooltipBehavior;
private static final Method installTooltipBehavior;
static {
try {
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
createTooltipBehavior.setAccessible(true);
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installTooltipBehavior.setAccessible(true);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
installTooltipBehavior.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
LOG.warning("Failed to set tooltip show delay", e);
Tooltip.install(node, tooltip);
}
}
}
// For Java 9+
private static final class NewInstaller extends TooltipInstaller {
private static final MethodHandle setTooltipShowDelay;
private static final MethodHandle setTooltipShowDuration;
private static final MethodHandle setTooltipHideDelay;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodType methodType = MethodType.methodType(void.class, Duration.class);
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
setTooltipShowDelay.invokeExact(tooltip, showDelay);
setTooltipShowDuration.invokeExact(tooltip, showDuration);
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
} catch (Throwable e) {
LOG.warning("Failed to set tooltip show delay", e);
}
Tooltip.install(node, tooltip);
}
}
}
@@ -48,11 +48,22 @@ public final class UpgradeDialog extends JFXDialogLayout {
setBody(new ProgressIndicator());
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
boolean isPreview = remoteVersion.isPreview();
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Node node = document.selectFirst("#nowchange");
String id = null;
Node node = null;
if (isPreview) {
id = "nowpreview";
node = document.selectFirst("#" + id);
}
if (node == null) {
id = "nowchange";
node = document.selectFirst("#" + id);
}
if (node == null || !"h1".equals(node.nodeName()))
throw new IOException("Cannot find #nowchange in document");
throw new IOException("Cannot find current changelog in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
LOG.info("Open link: " + uri);
@@ -60,7 +71,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
});
do {
if ("h1".equals(node.nodeName()) && !"nowchange".equals(node.attr("id"))) {
if ("h1".equals(node.nodeName()) && !id.equals(node.attr("id"))) {
break;
}
renderer.appendNode(node);
@@ -78,15 +78,22 @@ public class AccountAdvancedListItem extends AdvancedListItem {
setActionButtonVisible(false);
setOnScroll(event -> {
double deltaY = event.getDeltaY();
if (deltaY == 0)
return;
Account current = account.get();
if (current == null) return;
ObservableList<Account> accounts = Accounts.getAccounts();
int currentIndex = accounts.indexOf(account.get());
if (event.getDeltaY() > 0) { // up
int currentIndex = accounts.indexOf(current);
if (currentIndex < 0) return;
if (deltaY > 0) // up
currentIndex--;
} else { // down
else // down
currentIndex++;
}
Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size()));
});
}
@@ -33,7 +33,6 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
@@ -129,10 +128,6 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnRefresh = new JFXButton();
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
btnRefresh.setDisable(true);
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot"));
}
btnRefresh.setOnAction(e -> {
spinnerRefresh.showSpinner();
skinnable.refreshAsync()
@@ -41,7 +41,6 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jackhuang.hmcl.util.platform.NativeUtils;
@@ -49,6 +48,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
import java.net.URI;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Locale;
@@ -190,7 +190,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
item.titleProperty().bind(title);
String host = "";
try {
host = NetworkUtils.toURI(server.getUrl()).getHost();
host = URI.create(server.getUrl()).getHost();
} catch (IllegalArgumentException e) {
LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e);
}
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import javax.net.ssl.SSLException;
import java.io.IOException;
@@ -195,8 +196,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl());
//noinspection HttpUrlsUsage
lblServerWarning.setVisible(serverBeingAdded.getUrl().startsWith("http://"));
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
} else {
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.animation;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
/**
@@ -37,7 +36,7 @@ public final class AnimationUtils {
public static void init() {
}
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled() && FXUtils.REDUCED_MOTION != Boolean.TRUE;
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled();
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
public static boolean isAnimationEnabled() {
@@ -30,8 +30,7 @@ public class TransitionPane extends StackPane implements AnimationHandler {
private Duration duration;
private Node previousNode, currentNode;
{
currentNode = getChildren().stream().findFirst().orElse(null);
public TransitionPane() {
FXUtils.setOverflowHidden(this);
}
@@ -91,8 +90,12 @@ public class TransitionPane extends StackPane implements AnimationHandler {
private void updateContent(Node newView) {
if (getWidth() > 0 && getHeight() > 0) {
previousNode = currentNode;
if (previousNode == null)
previousNode = EMPTY_PANE;
if (previousNode == null) {
if (getChildren().isEmpty())
previousNode = EMPTY_PANE;
else
previousNode = getChildren().get(0);
}
} else
previousNode = EMPTY_PANE;
@@ -98,7 +98,7 @@ public final class MessageDialogPane extends HBox {
{
StackPane titlePane = new StackPane();
titlePane.getStyleClass().addAll("jfx-layout-heading", "title");
titlePane.getChildren().setAll(new Label(title != null ? title : i18n("message.info")));
titlePane.getChildren().setAll(new Label(title != null ? title : type.getDisplayName()));
StackPane content = new StackPane();
content.getStyleClass().add("jfx-layout-body");
@@ -108,7 +108,6 @@ public final class MessageDialogPane extends HBox {
content.getChildren().setAll(textFlow);
else {
ScrollPane scrollPane = new ScrollPane(textFlow);
FXUtils.smoothScrolling(scrollPane);
scrollPane.setPrefHeight(350);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
scrollPane.setFitToWidth(true);
@@ -24,13 +24,11 @@ import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Label;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Paint;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils;
@@ -299,35 +297,4 @@ public final class MultiFileItem<T> extends VBox {
return pane;
}
}
public static final class PaintOption<T> extends Option<T> {
private final ColorPicker colorPicker = new ColorPicker();
public PaintOption(String title, T data) {
super(title, data);
}
public PaintOption<T> bindBidirectional(Property<Paint> property) {
FXUtils.bindPaint(colorPicker, property);
return this;
}
@Override
protected Node createItem(ToggleGroup group) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(3));
FXUtils.setLimitHeight(pane, 30);
left.setText(title);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
left.setToggleGroup(group);
left.setUserData(data);
pane.setLeft(left);
colorPicker.disableProperty().bind(left.selectedProperty().not());
BorderPane.setAlignment(colorPicker, Pos.CENTER_RIGHT);
pane.setRight(colorPicker);
return pane;
}
}
}
@@ -26,7 +26,9 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jetbrains.annotations.NotNull;
@@ -41,7 +43,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class TaskExecutorDialogPane extends BorderPane {
private TaskExecutor executor;
private TaskCancellationAction onCancel;
private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
private final Label lblTitle;
private final Label lblProgress;
@@ -106,7 +108,7 @@ public class TaskExecutorDialogPane extends BorderPane {
String finalUnit = unit;
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
};
FileDownloadTask.speedEvent.channel(FetchTask.SpeedEvent.class).registerWeak(speedEventHandler);
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
onEscPressed(this, btnCancel::fire);
}
@@ -261,6 +261,7 @@ public final class TaskListPane extends StackPane {
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
case "hmcl.install.quilt-api": message = i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue); break;
default: message = i18n(stageKey); break;
}
// @formatter:on
@@ -21,7 +21,10 @@ import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -55,9 +58,9 @@ public class URLValidator extends ValidatorBase {
hasErrors.set(!nullable);
else {
try {
NetworkUtils.toURI(textField.getText());
new URL(textField.getText()).toURI();
hasErrors.set(false);
} catch (IllegalArgumentException e) {
} catch (IOException | URISyntaxException e) {
hasErrors.set(true);
}
}
@@ -31,13 +31,12 @@ import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.*;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
@@ -45,7 +44,6 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
@@ -57,22 +55,26 @@ import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
public class DecoratorController {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
@@ -122,13 +124,18 @@ public class DecoratorController {
// Setup background
decorator.setContentBackground(getBackground());
changeBackgroundListener = o -> updateBackground();
changeBackgroundListener = o -> {
final int currentCount = ++this.changeBackgroundCount;
CompletableFuture.supplyAsync(this::getBackground, Schedulers.io())
.thenAcceptAsync(background -> {
if (this.changeBackgroundCount == currentCount)
decorator.setContentBackground(background);
}, Schedulers.javafx());
};
WeakInvalidationListener weakListener = new WeakInvalidationListener(changeBackgroundListener);
config().backgroundImageTypeProperty().addListener(weakListener);
config().backgroundImageProperty().addListener(weakListener);
config().backgroundImageUrlProperty().addListener(weakListener);
config().backgroundPaintProperty().addListener(weakListener);
config().backgroundImageOpacityProperty().addListener(weakListener);
// pass key events to current dialog / current page
decorator.addEventFilter(KeyEvent.ANY, e -> {
@@ -164,14 +171,6 @@ public class DecoratorController {
// press ESC to go back
onEscPressed(navigator, this::back);
// press F11 to toggle full screen
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.F11) {
stage.setFullScreen(!stage.isFullScreen());
e.consume();
}
});
try {
// For JavaFX 12+
MouseButton button = MouseButton.valueOf("BACK");
@@ -197,22 +196,10 @@ public class DecoratorController {
@SuppressWarnings("FieldCanBeLocal") // Strong reference
private final InvalidationListener changeBackgroundListener;
private void updateBackground() {
final int currentCount = ++this.changeBackgroundCount;
Task.supplyAsync(Schedulers.io(), this::getBackground)
.setName("Update background")
.whenComplete(Schedulers.javafx(), (background, exception) -> {
if (exception == null) {
if (this.changeBackgroundCount == currentCount)
decorator.setContentBackground(background);
} else {
LOG.warning("Failed to update background", exception);
}
}).start();
}
private Background getBackground() {
EnumBackgroundImage imageType = config().getBackgroundImageType();
if (imageType == null)
imageType = EnumBackgroundImage.DEFAULT;
Image image = null;
switch (imageType) {
@@ -229,7 +216,7 @@ public class DecoratorController {
String backgroundImageUrl = config().getBackgroundImageUrl();
if (backgroundImageUrl != null) {
try {
image = FXUtils.loadImage(backgroundImageUrl);
image = FXUtils.loadImage(new URL(backgroundImageUrl));
} catch (Exception e) {
LOG.warning("Couldn't load background image", e);
}
@@ -238,60 +225,13 @@ public class DecoratorController {
case CLASSIC:
image = newBuiltinImage("/assets/img/background-classic.jpg");
break;
case TRANSLUCENT: // Deprecated
case TRANSLUCENT:
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
case PAINT:
Paint paint = config().getBackgroundPaint();
double opacity = Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.;
if (paint instanceof Color || paint == null) {
Color color = (Color) paint;
if (color == null)
color = Color.WHITE; // Default to white if no color is set
if (opacity < 1.)
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), opacity);
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
} else {
// TODO: Support opacity for non-color paints
return new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY));
}
}
if (image == null) {
image = loadDefaultBackgroundImage();
}
return createBackgroundWithOpacity(image, config().getBackgroundImageOpacity());
}
private Background createBackgroundWithOpacity(Image image, int opacity) {
if (opacity <= 0) {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
} else if (opacity >= 100) {
return new Background(new BackgroundImage(
image,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(800, 480, false, false, true, true)
));
} else {
WritableImage tempImage = new WritableImage((int) image.getWidth(), (int) image.getHeight());
PixelReader pixelReader = image.getPixelReader();
PixelWriter pixelWriter = tempImage.getPixelWriter();
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
Color color = pixelReader.getColor(x, y);
Color newColor = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity / 100);
pixelWriter.setColor(x, y, newColor);
}
}
return new Background(new BackgroundImage(
tempImage,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(800, 480, false, false, true, true)
));
}
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
}
/**
@@ -18,11 +18,8 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -32,7 +29,6 @@ import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
@@ -54,8 +50,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
private final Stage primaryStage;
private final TransitionPane navBarPane;
private final InvalidationListener onWindowsStatusChange;
private double mouseInitX, mouseInitY, stageInitX, stageInitY, stageInitWidth, stageInitHeight;
/**
@@ -86,25 +80,9 @@ public class DecoratorSkin extends SkinBase<Decorator> {
skinnable.getSnackbar().registerSnackbarContainer(parent);
EventHandler<MouseEvent> onMouseReleased = this::onMouseReleased;
EventHandler<MouseEvent> onMouseDragged = this::onMouseDragged;
EventHandler<MouseEvent> onMouseMoved = this::onMouseMoved;
onWindowsStatusChange = observable -> {
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
} else {
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
}
};
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
onWindowsStatusChange.invalidated(null);
root.addEventFilter(MouseEvent.MOUSE_RELEASED, this::onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, this::onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
shadowContainer.getChildren().setAll(parent);
root.getChildren().setAll(shadowContainer);
@@ -177,12 +155,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
BorderPane titleBar = new BorderPane();
titleContainer.getChildren().add(titleBar);
titleBar.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
primaryStage.setMaximized(!primaryStage.isMaximized());
event.consume();
}
});
Rectangle buttonsContainerPlaceHolder = new Rectangle();
{
@@ -124,7 +124,7 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
libraryPane.setVgap(16);
libraryPane.setHgap(16);
if (libraries.length <= 8) {
if (FXUtils.JAVAFX_MAJOR_VERSION < 25 && libraries.length <= 8) {
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
root.setCenter(libraryPane);
} else {
@@ -133,6 +133,9 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
scrollPane.setFitToHeight(true);
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
root.setCenter(scrollPane);
if (libraries.length <= 8)
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
}
}
@@ -52,6 +52,7 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
@@ -155,7 +156,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
Controllers.taskDialog(Task.composeAsync(() -> {
var task = new FileDownloadTask(file.getFile().getUrl(), dest);
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Map;
@@ -76,24 +77,11 @@ public class InstallersPage extends AbstractInstallersPage {
public void cleanup(Map<String, Object> settings) {
}
private static boolean checkName(String name) {
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (!(c >= '0' && c <= '9')
&& !(c >= 'a' && c <= 'z')
&& !(c >= 'A' && c <= 'Z')
&& c != '-' && c != '_' && c != '.'
)
return false;
}
return true;
}
protected void onInstall() {
String name = txtName.getText();
if (!checkName(name)) {
// Check for non-ASCII characters.
if (!StringUtils.isASCII(name)) {
Controllers.dialog(new MessageDialogPane.Builder(
i18n("install.name.invalid"),
i18n("message.warning"),
@@ -29,7 +29,9 @@ import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -41,6 +43,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@@ -123,9 +126,10 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
}
private void onChooseRemoteFile() {
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, resolve, reject) -> {
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> {
try {
if (url.endsWith("server-manifest.json")) {
URL url = new URL(urlString);
if (urlString.endsWith("server-manifest.json")) {
// if urlString ends with .json, we assume that the url is server-manifest.json
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
@@ -146,7 +150,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
resolve.run();
Controllers.taskDialog(
new FileDownloadTask(url, modpack)
new FileDownloadTask(url, modpack.toFile(), null)
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
resolve.run();
@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -133,28 +133,28 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) exception.getCause();
int responseCode = rce.getResponseCode();
String uri = rce.getUri();
URL url = rce.getUrl();
if (responseCode == 404)
message += i18n("download.code.404", uri);
message += i18n("download.code.404", url);
else
message += i18n("download.failed", uri, responseCode);
message += i18n("download.failed", url, responseCode);
} else {
message += StringUtils.getStackTrace(exception.getCause());
}
Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception instanceof DownloadException) {
URI uri = ((DownloadException) exception).getUri();
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
Controllers.dialog(i18n("install.failed.downloading.timeout", uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.timeout", url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
}
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
}
} else if (exception instanceof UnsupportedInstallationException) {
switch (((UnsupportedInstallationException) exception).getReason()) {
@@ -17,17 +17,24 @@
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXSpinner;
import com.jfoenix.controls.JFXTextField;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
@@ -42,73 +49,295 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.ui.FXUtils.*;
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VersionsPage extends Control implements WizardPage, Refreshable {
public final class VersionsPage extends BorderPane implements WizardPage, Refreshable {
private final String gameVersion;
private final String libraryId;
private final String title;
private final Navigation navigation;
private final VersionList<?> versionList;
private final Runnable callback;
private final ObservableList<RemoteVersion> versions = FXCollections.observableArrayList();
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
private final JFXListView<RemoteVersion> list;
private final JFXSpinner spinner;
private final StackPane failedPane;
private final StackPane emptyPane;
private final TransitionPane root;
private final JFXCheckBox chkRelease;
private final JFXCheckBox chkSnapshot;
private final JFXCheckBox chkOld;
private final ComponentList centrePane;
private final StackPane center;
private final VersionList<?> versionList;
private CompletableFuture<?> executor;
private final HBox searchBar;
private final StringProperty queryString = new SimpleStringProperty();
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
this.title = title;
this.gameVersion = gameVersion;
this.libraryId = libraryId;
this.navigation = navigation;
this.versionList = downloadProvider.getVersionListById(libraryId);
this.callback = callback;
HintPane hintPane = new HintPane();
hintPane.setText(i18n("sponsor.bmclapi"));
hintPane.getStyleClass().add("sponsor-pane");
FXUtils.onClicked(hintPane, this::onSponsor);
BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10));
this.setTop(hintPane);
root = new TransitionPane();
BorderPane toolbarPane = new BorderPane();
JFXButton btnRefresh;
{
spinner = new JFXSpinner();
center = new StackPane();
center.setStyle("-fx-padding: 10;");
{
centrePane = new ComponentList();
centrePane.getStyleClass().add("no-padding");
{
HBox checkPane = new HBox();
checkPane.setSpacing(10);
{
chkRelease = new JFXCheckBox(i18n("version.game.releases"));
chkRelease.setSelected(true);
HBox.setMargin(chkRelease, new Insets(10, 0, 10, 0));
chkSnapshot = new JFXCheckBox(i18n("version.game.snapshots"));
HBox.setMargin(chkSnapshot, new Insets(10, 0, 10, 0));
chkOld = new JFXCheckBox(i18n("version.game.old"));
HBox.setMargin(chkOld, new Insets(10, 0, 10, 0));
checkPane.getChildren().setAll(chkRelease, chkSnapshot, chkOld);
}
list = new JFXListView<>();
list.getStyleClass().add("jfx-list-view-float");
VBox.setVgrow(list, Priority.ALWAYS);
TransitionPane rightToolbarPane = new TransitionPane();
{
HBox refreshPane = new HBox();
refreshPane.setAlignment(Pos.CENTER_RIGHT);
btnRefresh = new JFXButton(i18n("button.refresh"));
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
btnRefresh.setOnAction(e -> onRefresh());
JFXButton btnSearch = new JFXButton(i18n("search"));
btnSearch.getStyleClass().add("jfx-tool-bar-button");
btnSearch.setGraphic(wrap(SVG.SEARCH.createIcon(Theme.blackFill(), -1)));
searchBar = new HBox();
{
searchBar.setAlignment(Pos.CENTER);
searchBar.setPadding(new Insets(0, 5, 0, 0));
JFXTextField searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
JFXButton closeSearchBar = new JFXButton();
closeSearchBar.getStyleClass().add("jfx-tool-bar-button");
closeSearchBar.setGraphic(wrap(SVG.CLOSE.createIcon(Theme.blackFill(), -1)));
closeSearchBar.setOnAction(e -> {
searchField.clear();
rightToolbarPane.setContent(refreshPane, ContainerAnimations.FADE);
});
onEscPressed(searchField, closeSearchBar::fire);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> queryString.set(searchField.getText()));
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
});
searchBar.getChildren().setAll(searchField, closeSearchBar);
btnSearch.setOnAction(e -> {
rightToolbarPane.setContent(searchBar, ContainerAnimations.FADE);
searchField.requestFocus();
});
}
refreshPane.getChildren().setAll(new HBox(btnSearch, btnRefresh));
rightToolbarPane.setContent(refreshPane, ContainerAnimations.NONE);
}
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
toolbarPane.setLeft(checkPane);
toolbarPane.setRight(rightToolbarPane);
centrePane.getContent().setAll(toolbarPane, list);
}
center.getChildren().setAll(centrePane);
}
failedPane = new StackPane();
failedPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.refresh"));
FXUtils.onClicked(label, this::onRefresh);
failedPane.getChildren().setAll(label);
}
emptyPane = new StackPane();
emptyPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.empty"));
FXUtils.onClicked(label, this::onBack);
emptyPane.getChildren().setAll(label);
}
}
this.setCenter(root);
versionList = downloadProvider.getVersionListById(libraryId);
boolean hasType = versionList.hasType();
chkRelease.setManaged(hasType);
chkRelease.setVisible(hasType);
chkSnapshot.setManaged(hasType);
chkSnapshot.setVisible(hasType);
chkOld.setManaged(hasType);
chkOld.setVisible(hasType);
if (hasType) {
centrePane.getContent().setAll(toolbarPane, list);
} else {
centrePane.getContent().setAll(list);
}
ComponentList.setVgrow(list, Priority.ALWAYS);
InvalidationListener listener = o -> {
List<RemoteVersion> versions = loadVersions();
String query = queryString.get();
if (!StringUtils.isBlank(query)) {
Predicate<RemoteVersion> predicate;
if (query.startsWith("regex:")) {
try {
Pattern pattern = Pattern.compile(query.substring("regex:".length()));
predicate = it -> pattern.matcher(it.getSelfVersion()).find();
} catch (Throwable e) {
LOG.warning("Illegal regular expression", e);
return;
}
} else {
String lowerQueryString = query.toLowerCase(Locale.ROOT);
predicate = it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString);
}
versions = versions.stream().filter(predicate).collect(Collectors.toList());
}
list.getItems().setAll(versions);
};
chkRelease.selectedProperty().addListener(listener);
chkSnapshot.selectedProperty().addListener(listener);
chkOld.selectedProperty().addListener(listener);
queryString.addListener(listener);
btnRefresh.setGraphic(wrap(SVG.REFRESH.createIcon(Theme.blackFill(), -1)));
Holder<RemoteVersionListCell> lastCell = new Holder<>();
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, libraryId));
FXUtils.onClicked(list, () -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
navigation.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem());
callback.run();
});
refresh();
}
@Override
protected Skin<?> createDefaultSkin() {
return new VersionsPageSkin(this);
private List<RemoteVersion> loadVersions() {
return versionList.getVersions(gameVersion).stream()
.filter(it -> {
switch (it.getVersionType()) {
case RELEASE:
return chkRelease.isSelected();
case PENDING:
case SNAPSHOT:
return chkSnapshot.isSelected();
case OLD:
return chkOld.isSelected();
default:
return true;
}
})
.sorted().collect(Collectors.toList());
}
@Override
public void refresh() {
status.set(Status.LOADING);
Task<?> task = versionList.refreshAsync(gameVersion)
.thenSupplyAsync(() -> versionList.getVersions(gameVersion).stream().sorted().collect(Collectors.toList()))
.whenComplete(Schedulers.javafx(), (items, exception) -> {
if (exception == null) {
versions.setAll(items);
status.set(Status.SUCCESS);
VersionList<?> currentVersionList = versionList;
root.setContent(spinner, ContainerAnimations.FADE);
executor = currentVersionList.refreshAsync(gameVersion).whenComplete((result, exception) -> {
if (exception == null) {
List<RemoteVersion> items = loadVersions();
Platform.runLater(() -> {
if (versionList != currentVersionList) return;
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
root.setContent(emptyPane, ContainerAnimations.FADE);
} else {
LOG.warning("Failed to fetch versions list", exception);
status.set(Status.FAILED);
if (items.isEmpty()) {
chkRelease.setSelected(true);
chkSnapshot.setSelected(true);
chkOld.setSelected(true);
} else {
list.getItems().setAll(items);
}
root.setContent(center, ContainerAnimations.FADE);
}
});
task.start();
} else {
LOG.warning("Failed to fetch versions list", exception);
Platform.runLater(() -> {
if (versionList != currentVersionList) return;
root.setContent(failedPane, ContainerAnimations.FADE);
});
}
// https://github.com/HMCL-dev/HMCL/issues/938
System.gc();
});
}
@Override
@@ -119,6 +348,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(libraryId);
if (executor != null)
executor.cancel(true);
}
private void onRefresh() {
@@ -133,24 +364,10 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
FXUtils.openLink("https://bmclapidoc.bangbang93.com");
}
private enum Status {
LOADING,
FAILED,
SUCCESS,
}
private enum VersionTypeFilter {
ALL,
RELEASE,
SNAPSHOTS,
APRIL_FOOLS,
OLD
}
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
private final IconedTwoLineListItem content = new IconedTwoLineListItem();
private final RipplerContainer ripplerContainer = new RipplerContainer(content);
private final StackPane pane = new StackPane();
final IconedTwoLineListItem content = new IconedTwoLineListItem();
final RipplerContainer ripplerContainer = new RipplerContainer(content);
final StackPane pane = new StackPane();
private final Holder<RemoteVersionListCell> lastCell;
@@ -189,30 +406,22 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
}
if (remoteVersion instanceof GameRemoteVersion) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
String wikiSuffix = getWikiUrlSuffix(remoteVersion.getGameVersion());
switch (versionType) {
switch (remoteVersion.getVersionType()) {
case RELEASE:
content.getTags().setAll(i18n("version.game.release"));
content.setImage(VersionIconType.GRASS.getIcon());
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
content.setExternalLink(i18n("wiki.version.game.release", remoteVersion.getGameVersion()));
break;
case PENDING:
case SNAPSHOT:
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isSpecial()) {
content.getTags().setAll(i18n("version.game.april_fools"));
content.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(VersionIconType.COMMAND.getIcon());
}
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(VersionIconType.COMMAND.getIcon());
content.setExternalLink(i18n("wiki.version.game.snapshot", remoteVersion.getGameVersion()));
break;
default:
content.getTags().setAll(i18n("version.game.old"));
content.setImage(VersionIconType.CRAFT_TABLE.getIcon());
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
content.setExternalLink(null);
break;
}
} else {
@@ -240,285 +449,5 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
content.setExternalLink(null);
}
}
private String getWikiUrlSuffix(String gameVersion) {
String id = gameVersion.toLowerCase(Locale.ROOT);
switch (id) {
case "0.30-1":
case "0.30-2":
case "c0.30_01c":
return i18n("wiki.version.game.search", "Classic_0.30");
case "in-20100206-2103":
return i18n("wiki.version.game.search", "Indev_20100206");
case "inf-20100630-1":
return i18n("wiki.version.game.search", "Infdev_20100630");
case "inf-20100630-2":
return i18n("wiki.version.game.search", "Alpha_v1.0.0");
case "1.19_deep_dark_experimental_snapshot-1":
return "1.19-exp1";
case "in-20100130":
return i18n("wiki.version.game.search", "Indev_0.31_20100130");
case "b1.6-tb3":
return i18n("wiki.version.game.search", "Beta_1.6_Test_Build_3");
case "1.14_combat-212796":
return i18n("wiki.version.game.search", "1.14.3_-_Combat_Test");
case "1.14_combat-0":
return i18n("wiki.version.game.search", "Combat_Test_2");
case "1.14_combat-3":
return i18n("wiki.version.game.search", "Combat_Test_3");
case "1_15_combat-1":
return i18n("wiki.version.game.search", "Combat_Test_4");
case "1_15_combat-6":
return i18n("wiki.version.game.search", "Combat_Test_5");
case "1_16_combat-0":
return i18n("wiki.version.game.search", "Combat_Test_6");
case "1_16_combat-1":
return i18n("wiki.version.game.search", "Combat_Test_7");
case "1_16_combat-2":
return i18n("wiki.version.game.search", "Combat_Test_7b");
case "1_16_combat-3":
return i18n("wiki.version.game.search", "Combat_Test_7c");
case "1_16_combat-4":
return i18n("wiki.version.game.search", "Combat_Test_8");
case "1_16_combat-5":
return i18n("wiki.version.game.search", "Combat_Test_8b");
case "1_16_combat-6":
return i18n("wiki.version.game.search", "Combat_Test_8c");
}
if (id.startsWith("1.0.0-rc2")) return "RC2";
if (id.startsWith("2.0")) return i18n("wiki.version.game.search", "2.0");
if (id.startsWith("b1.8-pre1")) return "Beta_1.8-pre1";
if (id.startsWith("b1.1-")) return i18n("wiki.version.game.search", "Beta_1.1");
if (id.startsWith("a1.1.0")) return "Alpha_v1.1.0";
if (id.startsWith("a1.0.14")) return "Alpha_v1.0.14";
if (id.startsWith("a1.0.13_01")) return "Alpha_v1.0.13_01";
if (id.startsWith("in-20100214")) return i18n("wiki.version.game.search", "Indev_20100214");
if (id.contains("experimental-snapshot")) {
return id.replace("_experimental-snapshot-", "-exp");
}
if (id.startsWith("inf-")) return id.replace("inf-", "Infdev_");
if (id.startsWith("in-")) return id.replace("in-", "Indev_");
if (id.startsWith("rd-")) return "pre-Classic_" + id;
if (id.startsWith("b")) return id.replace("b", "Beta_");
if (id.startsWith("a")) return id.replace("a", "Alpha_v");
if (id.startsWith("c")) return id.replace("c", "Classic_").replace("st", "SURVIVAL_TEST");
return i18n("wiki.version.game.search", id);
}
}
private static final class VersionsPageSkin extends SkinBase<VersionsPage> {
private final JFXListView<RemoteVersion> list;
private final TransitionPane transitionPane;
private final JFXSpinner spinner;
private final JFXTextField nameField;
private final JFXComboBox<VersionTypeFilter> categoryField = new JFXComboBox<>();
VersionsPageSkin(VersionsPage control) {
super(control);
BorderPane root = new BorderPane();
GridPane searchPane = new GridPane();
root.setTop(searchPane);
searchPane.getStyleClass().addAll("card");
BorderPane.setMargin(searchPane, new Insets(10, 10, 0, 10));
ColumnConstraints nameColumn = new ColumnConstraints();
nameColumn.setMinWidth(USE_PREF_SIZE);
ColumnConstraints column1 = new ColumnConstraints();
column1.setHgrow(Priority.ALWAYS);
ColumnConstraints column2 = new ColumnConstraints();
column2.setMaxWidth(150);
ColumnConstraints column3 = new ColumnConstraints();
if (control.versionList.hasType())
searchPane.getColumnConstraints().setAll(nameColumn, column1, nameColumn, column2, column3);
else
searchPane.getColumnConstraints().setAll(nameColumn, column1, column3);
searchPane.setHgap(16);
searchPane.setVgap(10);
{
int rowIndex = 0;
{
nameField = new JFXTextField();
nameField.setPromptText(i18n("version.search.prompt"));
nameField.textProperty().addListener(o -> updateList());
if ("game".equals(control.libraryId)) {
categoryField.getItems().setAll(
VersionTypeFilter.ALL,
VersionTypeFilter.RELEASE,
VersionTypeFilter.SNAPSHOTS,
VersionTypeFilter.APRIL_FOOLS,
VersionTypeFilter.OLD
);
categoryField.getSelectionModel().select(VersionTypeFilter.RELEASE);
} else {
categoryField.getItems().setAll(
VersionTypeFilter.ALL,
VersionTypeFilter.RELEASE,
VersionTypeFilter.SNAPSHOTS
);
categoryField.getSelectionModel().select(VersionTypeFilter.ALL);
}
categoryField.setConverter(stringConverter(type -> i18n("version.game." + type.name().toLowerCase(Locale.ROOT))));
categoryField.getSelectionModel().selectedItemProperty().addListener(o -> updateList());
JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
refreshButton.setOnAction(event -> control.onRefresh());
if (control.versionList.hasType()) {
searchPane.addRow(rowIndex++,
new Label(i18n("version.search")), nameField,
new Label(i18n("version.game.type")), categoryField,
refreshButton
);
} else {
searchPane.addRow(rowIndex++,
new Label(i18n("version.search")), nameField,
refreshButton
);
}
}
// {
// HBox actionsBox = new HBox(8);
// GridPane.setColumnSpan(actionsBox, 4);
// actionsBox.setAlignment(Pos.CENTER_RIGHT);
//
// JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
// refreshButton.setOnAction(event -> control.onRefresh());
//
// actionsBox.getChildren().setAll(refreshButton);
//
// searchPane.addRow(rowIndex++, actionsBox);
// }
}
{
SpinnerPane spinnerPane = new SpinnerPane();
root.setCenter(spinnerPane);
transitionPane = new TransitionPane();
spinner = new JFXSpinner();
StackPane centerWrapper = new StackPane();
centerWrapper.setStyle("-fx-padding: 10;");
{
ComponentList centrePane = new ComponentList();
centrePane.getStyleClass().add("no-padding");
{
list = new JFXListView<>();
list.getStyleClass().add("jfx-list-view-float");
VBox.setVgrow(list, Priority.ALWAYS);
control.versions.addListener((InvalidationListener) o -> updateList());
Holder<RemoteVersionListCell> lastCell = new Holder<>();
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control.libraryId));
FXUtils.onClicked(list, () -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
control.navigation.getSettings().put(control.libraryId, list.getSelectionModel().getSelectedItem());
control.callback.run();
});
ComponentList.setVgrow(list, Priority.ALWAYS);
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
centrePane.getContent().setAll(list);
}
centerWrapper.getChildren().setAll(centrePane);
}
StackPane failedPane = new StackPane();
failedPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.refresh"));
FXUtils.onClicked(label, control::onRefresh);
failedPane.getChildren().setAll(label);
}
StackPane emptyPane = new StackPane();
emptyPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.empty"));
FXUtils.onClicked(label, control::onBack);
emptyPane.getChildren().setAll(label);
}
FXUtils.onChangeAndOperate(control.status, status -> {
if (status == Status.LOADING)
transitionPane.setContent(spinner, ContainerAnimations.FADE);
else if (status == Status.SUCCESS)
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
else // if (status == Status.FAILED)
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
});
root.setCenter(transitionPane);
}
this.getChildren().setAll(root);
}
private void updateList() {
Stream<RemoteVersion> versions = getSkinnable().versions.stream();
VersionTypeFilter filter = categoryField.getSelectionModel().getSelectedItem();
if (filter != null)
versions = versions.filter(it -> {
RemoteVersion.Type versionType = it.getVersionType();
switch (filter) {
case RELEASE:
return versionType == RemoteVersion.Type.RELEASE;
case SNAPSHOTS:
return versionType == RemoteVersion.Type.SNAPSHOT
|| versionType == RemoteVersion.Type.PENDING;
case APRIL_FOOLS:
return versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isSpecial();
case OLD:
return versionType == RemoteVersion.Type.OLD;
case ALL:
default:
return true;
}
});
String nameQuery = nameField.getText();
if (!StringUtils.isBlank(nameQuery)) {
if (nameQuery.startsWith("regex:")) {
try {
Pattern pattern = Pattern.compile(nameQuery.substring("regex:".length()));
versions = versions.filter(it -> pattern.matcher(it.getSelfVersion()).find());
} catch (Throwable e) {
LOG.warning("Illegal regular expression: " + nameQuery, e);
}
} else {
String lowerQueryString = nameQuery.toLowerCase(Locale.ROOT);
versions = versions.filter(it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString));
}
}
//noinspection DataFlowIssue
list.getItems().setAll(versions.collect(Collectors.toList()));
list.scrollTo(0);
}
}
}
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.FontManager;
import org.jackhuang.hmcl.setting.Profile;
@@ -101,9 +100,6 @@ public final class ExportWizardProvider implements WizardProvider {
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
exportTask = exportAsServer(exportInfo, dest);
break;
case ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH:
exportTask = exportAsModrinth(exportInfo, dest);
break;
default:
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
}
@@ -237,27 +233,6 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
private Task<?> exportAsModrinth(ModpackExportInfo exportInfo, File modpackFile) {
return new Task<Void>() {
Task<?> dependency;
@Override
public void execute() {
dependency = new ModrinthModpackExportTask(
profile.getRepository(),
version,
exportInfo,
modpackFile
);
}
@Override
public Collection<Task<?>> getDependencies() {
return Collections.singleton(dependency);
}
};
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
switch (step) {
@@ -53,7 +53,6 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -81,8 +80,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
private final SimpleStringProperty launchArguments = new SimpleStringProperty("");
private final SimpleStringProperty javaArguments = new SimpleStringProperty("");
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty("");
private final SimpleBooleanProperty noCreateRemoteFiles = new SimpleBooleanProperty();
private final SimpleBooleanProperty skipCurseForgeRemoteFiles = new SimpleBooleanProperty();
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
this.controller = controller;
@@ -105,13 +102,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
private void onNext() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
if (controller.getSettings().get(MODPACK_TYPE) == ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH) {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.mrpack"));
fileChooser.setInitialFileName(name.get() + ".mrpack");
} else {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
fileChooser.setInitialFileName(name.get() + ".zip");
}
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
File file = fileChooser.showSaveDialog(Controllers.getStage());
if (file == null) {
controller.onEnd();
@@ -131,8 +122,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
exportInfo.setLaunchArguments(launchArguments.get());
exportInfo.setJavaArguments(javaArguments.get());
exportInfo.setAuthlibInjectorServer(authlibInjectorServer.get());
exportInfo.setNoCreateRemoteFiles(noCreateRemoteFiles.get());
exportInfo.setSkipCurseForgeRemoteFiles(skipCurseForgeRemoteFiles.get());
if (StringUtils.isNotBlank(mcbbsThreadId.get())) {
exportInfo.setOrigins(Collections.singletonList(new McbbsModpackManifest.Origin(
@@ -189,10 +178,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
Hyperlink hyperlink = new Hyperlink(i18n("modpack.wizard.step.initialization.server"));
hyperlink.setOnAction(e -> FXUtils.openLink(Metadata.DOCS_URL + "/modpack/serverpack.html"));
borderPane.setTop(hyperlink);
} if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
HintPane pane = new HintPane(MessageDialogPane.MessageType.INFO);
pane.setText(i18n("modpack.wizard.step.initialization.modrinth.info"));
borderPane.setTop(pane);
} else {
HintPane pane = new HintPane(MessageDialogPane.MessageType.INFO);
pane.setText(i18n("modpack.wizard.step.initialization.warning"));
@@ -381,32 +366,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
button.setMaxHeight(16);
pane.setRight(button);
}
if (skinnable.options.isRequireNoCreateRemoteFiles()) {
BorderPane noCreateRemoteFiles = new BorderPane();
noCreateRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.no_create_remote_files")));
list.getContent().add(noCreateRemoteFiles);
JFXToggleButton noCreateRemoteFilesButton = new JFXToggleButton();
noCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
noCreateRemoteFilesButton.setSize(8);
noCreateRemoteFilesButton.setMinHeight(16);
noCreateRemoteFilesButton.setMaxHeight(16);
noCreateRemoteFiles.setRight(noCreateRemoteFilesButton);
}
if (skinnable.options.isRequireSkipCurseForgeRemoteFiles()) {
BorderPane skipCurseForgeRemoteFiles = new BorderPane();
skipCurseForgeRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files")));
list.getContent().add(skipCurseForgeRemoteFiles);
JFXToggleButton skipCurseForgeRemoteFilesButton = new JFXToggleButton();
skipCurseForgeRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.skipCurseForgeRemoteFiles);
skipCurseForgeRemoteFilesButton.setSize(8);
skipCurseForgeRemoteFilesButton.setMinHeight(16);
skipCurseForgeRemoteFilesButton.setMaxHeight(16);
skipCurseForgeRemoteFiles.setRight(skipCurseForgeRemoteFilesButton);
}
}
{
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -57,8 +56,7 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
title,
createButton(MODPACK_TYPE_MCBBS, McbbsModpackExportTask.OPTION),
createButton(MODPACK_TYPE_MULTIMC, MultiMCModpackExportTask.OPTION),
createButton(MODPACK_TYPE_SERVER, ServerModpackExportTask.OPTION),
createButton(MODPACK_TYPE_MODRINTH, ModrinthModpackExportTask.OPTION)
createButton(MODPACK_TYPE_SERVER, ServerModpackExportTask.OPTION)
);
}
@@ -102,5 +100,4 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
public static final String MODPACK_TYPE_MULTIMC = "multimc";
public static final String MODPACK_TYPE_SERVER = "server";
public static final String MODPACK_TYPE_MODRINTH = "modrinth";
}
@@ -1,28 +0,0 @@
/*
* 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;
import javafx.scene.image.Image;
import java.io.InputStream;
public interface ImageLoader {
Image load(InputStream input,
int requestedWidth, int requestedHeight,
boolean preserveRatio, boolean smooth) throws Exception;
}
@@ -1,411 +0,0 @@
/*
* 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;
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.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl;
import org.jackhuang.hmcl.util.SwingFXUtils;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class ImageUtils {
// ImageLoaders
public static final ImageLoader DEFAULT = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
Image image = new Image(input,
requestedWidth, requestedHeight,
preserveRatio, smooth);
if (image.isError())
throw image.getException();
return image;
};
public static final ImageLoader WEBP = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
WebPImageReaderSpi spi = new WebPImageReaderSpi();
ImageReader reader = spi.createReaderInstance(null);
BufferedImage bufferedImage;
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
reader.setInput(imageInput, true, true);
bufferedImage = reader.read(0, reader.getDefaultReadParam());
} finally {
reader.dispose();
}
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
};
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
if (!"true".equals(System.getProperty("hmcl.experimental.apng", "true")))
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;
} catch (PngException e) {
throw new IOException(e);
}
};
public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
"webp", WEBP,
"apng", APNG
);
public static final Map<String, ImageLoader> CONTENT_TYPE_TO_LOADER = Map.of(
"image/webp", WEBP,
"image/apng", APNG
);
public static final Set<String> DEFAULT_EXTS = Set.of(
"jpg", "jpeg", "bmp", "gif"
);
public static final Set<String> DEFAULT_CONTENT_TYPES = Set.of(
"image/jpeg", "image/bmp", "image/gif"
);
// ------
public static final int HEADER_BUFFER_SIZE = 1024;
private static final byte[] RIFF_HEADER = {'R', 'I', 'F', 'F'};
private static final byte[] WEBP_HEADER = {'W', 'E', 'B', 'P'};
public static boolean isWebP(byte[] headerBuffer) {
return headerBuffer.length > 12
&& Arrays.equals(headerBuffer, 0, 4, RIFF_HEADER, 0, 4)
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
}
private static final byte[] PNG_HEADER = {
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
};
private static final class PngChunkHeader {
private static final int IDAT_HEADER = 0x49444154;
private static final int acTL_HEADER = 0x6163544c;
private final int length;
private final int chunkType;
private PngChunkHeader(int length, int chunkType) {
this.length = length;
this.chunkType = chunkType;
}
private static @Nullable PngChunkHeader readHeader(ByteBuffer headerBuffer) {
if (headerBuffer.remaining() < 8)
return null;
int length = headerBuffer.getInt();
int chunkType = headerBuffer.getInt();
return new PngChunkHeader(length, chunkType);
}
}
public static boolean isApng(byte[] headerBuffer) {
if (headerBuffer.length <= 20)
return false;
if (!Arrays.equals(
headerBuffer, 0, 8,
PNG_HEADER, 0, 8))
return false;
ByteBuffer buffer = ByteBuffer.wrap(headerBuffer, 8, headerBuffer.length - 8);
PngChunkHeader header;
while ((header = PngChunkHeader.readHeader(buffer)) != null) {
// https://wiki.mozilla.org/APNG_Specification#Structure
// To be recognized as an APNG, an `acTL` chunk must appear in the stream before any `IDAT` chunks.
// The `acTL` structure is described below.
if (header.chunkType == PngChunkHeader.IDAT_HEADER)
break;
if (header.chunkType == PngChunkHeader.acTL_HEADER)
return true;
final int numBytes = header.length + 4;
if (buffer.remaining() > numBytes)
buffer.position(buffer.position() + numBytes);
else
break;
}
return false;
}
public static @Nullable ImageLoader guessLoader(byte[] headerBuffer) {
if (isWebP(headerBuffer))
return WEBP;
if (isApng(headerBuffer))
return APNG;
return null;
}
public static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("^\\s(?<type>image/[\\w-])");
// APNG
private static int[] scale(int[] pixels,
int sourceWidth, int sourceHeight,
int targetWidth, int targetHeight) {
assert pixels.length == sourceWidth * sourceHeight;
double xScale = ((double) sourceWidth) / targetWidth;
double yScale = ((double) sourceHeight) / targetHeight;
int[] result = new int[targetWidth * targetHeight];
for (int row = 0; row < targetHeight; row++) {
for (int col = 0; col < targetWidth; col++) {
int sourceX = (int) (col * xScale);
int sourceY = (int) (row * yScale);
int color = pixels[sourceY * sourceWidth + sourceX];
result[row * targetWidth + col] = color;
}
}
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() {
}
}
@@ -1,57 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng;
import org.jackhuang.hmcl.ui.image.apng.argb8888.*;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.map.PngMap;
import org.jackhuang.hmcl.ui.image.apng.map.PngMapReader;
import org.jackhuang.hmcl.ui.image.apng.reader.DefaultPngChunkReader;
import org.jackhuang.hmcl.ui.image.apng.reader.PngChunkProcessor;
import org.jackhuang.hmcl.ui.image.apng.reader.PngReadHelper;
import org.jackhuang.hmcl.ui.image.apng.reader.PngReader;
import org.jackhuang.hmcl.ui.image.apng.util.PngContainer;
import org.jackhuang.hmcl.ui.image.apng.util.PngContainerBuilder;
import java.io.InputStream;
/**
* Convenient one liners for loading PNG images.
*/
public class Png {
/**
* Read the provided stream and produce a PngMap of the data.
*
* @param is stream to read from
* @param sourceName optional name, mainly for debugging.
* @return PngMap of the data.
* @throws PngException
*/
public static PngMap readMap(InputStream is, String sourceName) throws PngException {
return PngReadHelper.read(is, new PngMapReader(sourceName));
}
public static PngContainer readContainer(InputStream is) throws PngException {
return PngReadHelper.read(is, new DefaultPngChunkReader<>(new PngContainerBuilder()));
}
public static <ResultT> ResultT read(InputStream is, PngReader<ResultT> reader) throws PngException {
return PngReadHelper.read(is, reader);
}
public static <ResultT> ResultT read(InputStream is, PngChunkProcessor<ResultT> processor) throws PngException {
return PngReadHelper.read(is, new DefaultPngChunkReader<>(processor));
}
public static Argb8888Bitmap readArgb8888Bitmap(InputStream is) throws PngException {
Argb8888Processor<Argb8888Bitmap> processor = new Argb8888Processor<>(new DefaultImageArgb8888Director());
return PngReadHelper.read(is, new DefaultPngChunkReader<>(processor));
}
public static Argb8888BitmapSequence readArgb8888BitmapSequence(InputStream is) throws PngException {
Argb8888Processor<Argb8888BitmapSequence> processor = new Argb8888Processor<>(new Argb8888BitmapSequenceDirector());
return PngReadHelper.read(is, new DefaultPngChunkReader<>(processor));
}
}
@@ -1,19 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng;
/**
* A PNG file has an animation type - most commonly, not animated.
* <p>
* This is primarily relevant to tracking whether the default image in a PNG
* is to be
* a) processed as normal (e.g. in a standard or "non-animated" PNG file);
* b) processed and used as the first frame in an animated PNG; or
* c) not processed and discarded (e.g. in an animated PNG which does not use the first frame).
*/
public enum PngAnimationType {
NOT_ANIMATED,
ANIMATED_KEEP_DEFAULT_IMAGE,
ANIMATED_DISCARD_DEFAULT_IMAGE;
}
@@ -1,123 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng;
/**
* One four-letter (32-byte) code identifying the type of a PNG chunk.
* <p>
* Common chunk codes are defined as constants that can be used in a switch statement.
* Users can add their own chunk codes separately.
*/
public class PngChunkCode {
public static final PngChunkCode IHDR = new PngChunkCode(PngConstants.IHDR_VALUE, "IHDR");
public static final PngChunkCode PLTE = new PngChunkCode(PngConstants.PLTE_VALUE, "PLTE");
public static final PngChunkCode IDAT = new PngChunkCode(PngConstants.IDAT_VALUE, "IDAT");
public static final PngChunkCode IEND = new PngChunkCode(PngConstants.IEND_VALUE, "IEND");
public static final PngChunkCode gAMA = new PngChunkCode(PngConstants.gAMA_VALUE, "gAMA");
public static final PngChunkCode bKGD = new PngChunkCode(PngConstants.bKGD_VALUE, "bKGD");
public static final PngChunkCode tRNS = new PngChunkCode(PngConstants.tRNS_VALUE, "tRNS");
public static final PngChunkCode acTL = new PngChunkCode(PngConstants.acTL_VALUE, "acTL");
public static final PngChunkCode fcTL = new PngChunkCode(PngConstants.fcTL_VALUE, "fcTL");
public static final PngChunkCode fdAT = new PngChunkCode(PngConstants.fdAT_VALUE, "fdAT");
public final int numeric;
public final String letters;
PngChunkCode(int numeric, String letters) {
this.numeric = numeric;
this.letters = letters;
}
/**
* Find out if the chunk is "critical" as defined by http://www.w3.org/TR/PNG/#5Chunk-naming-conventions
*
* <pre>
* cHNk <-- 32 bit chunk type represented in text form
* ||||
* |||+- Safe-to-copy bit is 1 (lower case letter; bit 5 is 1)
* ||+-- Reserved bit is 0 (upper case letter; bit 5 is 0)
* |+--- Private bit is 0 (upper case letter; bit 5 is 0)
* +---- Ancillary bit is 1 (lower case letter; bit 5 is 1)
* </pre>
*
* @return true if this chunk is considered critical according to the PNG spec.
*/
public boolean isCritical() {
// Ancillary bit: bit 5 o first byte
return (numeric & PngConstants.BIT_CHUNK_IS_ANCILLARY) == 0;
}
public boolean isAncillary() {
// Ancillary bit: bit 5 of first byte
return (numeric & PngConstants.BIT_CHUNK_IS_ANCILLARY) > 0;
}
public boolean isPrivate() {
return (numeric & PngConstants.BIT_CHUNK_IS_PRIVATE) > 0;
}
public boolean isPublic() {
return (numeric & PngConstants.BIT_CHUNK_IS_PRIVATE) == 0;
}
public boolean isReserved() {
return (numeric & PngConstants.BIT_CHUNK_IS_RESERVED) > 0;
}
public boolean isSafeToCopy() {
return (numeric & PngConstants.BIT_CHUNK_IS_SAFE_TO_COPY) > 0;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PngChunkCode that = (PngChunkCode) o;
if (numeric != that.numeric) return false;
return !(letters != null ? !letters.equals(that.letters) : that.letters != null);
}
@Override
public int hashCode() {
int result = numeric;
result = 31 * result + (letters != null ? letters.hashCode() : 0);
return result;
}
@Override
public String toString() {
return letters + "(" + numeric + ")";
}
public static PngChunkCode from(int code) {
switch (code) {
case PngConstants.IHDR_VALUE:
return IHDR;
case PngConstants.gAMA_VALUE:
return gAMA;
case PngConstants.bKGD_VALUE:
return bKGD;
case PngConstants.tRNS_VALUE:
return tRNS;
case PngConstants.IDAT_VALUE:
return IDAT;
case PngConstants.IEND_VALUE:
return IEND;
case PngConstants.acTL_VALUE:
return acTL;
case PngConstants.fdAT_VALUE:
return fdAT;
default:
byte[] s = new byte[4];
s[0] = (byte) ((code & 0xff000000) >> 24);
s[1] = (byte) ((code & 0x00ff0000) >> 16);
s[2] = (byte) ((code & 0x0000ff00) >> 8);
s[3] = (byte) ((code & 0x000000ff));
return new PngChunkCode(code, new String(s));
}
}
}
@@ -1,61 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
/**
* PNG images support 5 colour types as defined at http://www.w3.org/TR/PNG/#11IHDR
*/
public enum PngColourType {
PNG_GREYSCALE(0, 1, "1, 2, 4, 8, 16", "Greyscale", "Each pixel is a greyscale sample"),
PNG_TRUECOLOUR(2, 3, "8, 16", "Truecolour", "Each pixel is an R,G,B triple"),
PNG_INDEXED_COLOUR(3, 1, "1, 2, 4, 8", "Indexed-colour", "Each pixel is a palette index; a PLTE chunk shall appear."),
PNG_GREYSCALE_WITH_ALPHA(4, 2, "4, 8, 16", "Greyscale with alpha", "Each pixel is a greyscale sample followed by an alpha sample."),
PNG_TRUECOLOUR_WITH_ALPHA(6, 4, "8, 16", "Truecolour with alpha", "Each pixel is an R,G,B triple followed by an alpha sample.");
public final int code;
public final int componentsPerPixel;
public final String allowedBitDepths;
public final String name;
public final String descriptino;
PngColourType(int code, int componentsPerPixel, String allowedBitDepths, String name, String descriptino) {
this.code = code;
this.componentsPerPixel = componentsPerPixel;
this.allowedBitDepths = allowedBitDepths;
this.name = name;
this.descriptino = descriptino;
}
public boolean isIndexed() {
return (code & 0x01) > 0;
}
public boolean hasAlpha() {
return (code & 0x04) > 0;
}
public boolean supportsSubByteDepth() {
return code == 0 || code == 3;
}
public static PngColourType fromByte(byte b) throws PngException {
switch (b) {
case 0:
return PNG_GREYSCALE;
case 2:
return PNG_TRUECOLOUR;
case 3:
return PNG_INDEXED_COLOUR;
case 4:
return PNG_GREYSCALE_WITH_ALPHA;
case 6:
return PNG_TRUECOLOUR_WITH_ALPHA;
default:
throw new PngIntegrityException(String.format("Valid PNG colour types are 0, 2, 3, 4, 6. Type '%d' is invalid", b));
}
}
}
@@ -1,120 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng;
public class PngConstants {
public static final int LENGTH_SIGNATURE = 8;
public static final int LENGTH_fcTL_CHUNK = 4 + 4 + 4 + 4 + 4 + 2 + 2 + 1 + 1;
public static final int LENGTH_acTL_CHUNK = 4 + 4;
public static final byte[] BYTES_SIGNATURE = new byte[]{
(byte) 0x89, 'P', 'N', 'G', (byte) 0x0D, (byte) 0x0A, (byte) 0x1A, (byte) 0x0A
};
public static final int IHDR_VALUE = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R'; // 1229472850;
public static final int PLTE_VALUE = 'P' << 24 | 'L' << 16 | 'T' << 8 | 'E';
public static final int IDAT_VALUE = 'I' << 24 | 'D' << 16 | 'A' << 8 | 'T'; // 1229209940;
public static final int IEND_VALUE = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D'; // 1229278788;
public static final int gAMA_VALUE = 'g' << 24 | 'A' << 16 | 'M' << 8 | 'A'; // 1732332865;
public static final int bKGD_VALUE = 'b' << 24 | 'K' << 16 | 'G' << 8 | 'D'; // 1649100612;
public static final int tRNS_VALUE = 't' << 24 | 'R' << 16 | 'N' << 8 | 'S';
public static final int acTL_VALUE = 'a' << 24 | 'c' << 16 | 'T' << 8 | 'L';
public static final int fcTL_VALUE = 'f' << 24 | 'c' << 16 | 'T' << 8 | 'L';
public static final int fdAT_VALUE = 'f' << 24 | 'd' << 16 | 'A' << 8 | 'T';
public static final int ERROR_NOT_PNG = 1;
public static final int ERROR_EOF = 2;
public static final int ERROR_EOF_EXPECTED = 3;
public static final int ERROR_UNKNOWN_IO_FAILURE = 4;
public static final int ERROR_FEATURE_NOT_SUPPORTED = 5;
public static final int ERROR_INTEGRITY = 6;
public static final int ONE_K = 1 << 10;
public static final int DEFAULT_TARGET_BUFFER_SIZE = 32 * ONE_K;
public static final byte[] GREY_PALETTE_16 = new byte[]{
(byte) 0x00, // 0
(byte) 0x11, // 1
(byte) 0x22, // 2
(byte) 0x33, // 3
(byte) 0x44, // 4
(byte) 0x55, // 5
(byte) 0x66, // 6
(byte) 0x77, // 7
(byte) 0x88, // 8
(byte) 0x99, // 9
(byte) 0xaa, // 10
(byte) 0xbb, // 11
(byte) 0xcc, // 12
(byte) 0xdd, // 13
(byte) 0xee, // 14
(byte) 0xff, // 15
};
// public static final byte[] MASKS_1 = new byte[] {
// (byte)0x01, // bit 0
// (byte)0x02, // bit 1
// (byte)0x04, // bit 2
// (byte)0x08, // bit 3
// (byte)0x10, // bit 4
// (byte)0x20, // bit 5
// (byte)0x40, // bit 6
// (byte)0x80, // bit 7
// };
//
// public static final byte[] MASKS_2 = new byte[] {
// (byte)0x03, // bit 0-1
// (byte)0x0C, // bit 2-3
// (byte)0x30, // bit 4-5
// (byte)0xC0, // bit 6-7
// };
//
// public static final byte[] MASKS_4 = new byte[] {
// (byte)0x0F, // bit 0-3
// (byte)0xF0, // bit 4-7
// };
public static final byte[] SHIFTS_1 = new byte[]{
(byte) 0, // bit 0
(byte) 1, // bit 1
(byte) 2, // bit 2
(byte) 3, // bit 3
(byte) 4, // bit 4
(byte) 5, // bit 5
(byte) 6, // bit 6
(byte) 7, // bit 7
};
public static final byte[] SHIFTS_2 = new byte[]{
(byte) 0, // bit 0-1
(byte) 2, // bit 2-3
(byte) 4, // bit 4-5
(byte) 6 // bit 6-7
};
public static final byte[] SHIFTS_4 = new byte[]{
(byte) 0x00, // bit 0-3
(byte) 0x04, // bit 4-7
};
public static final int BIT_CHUNK_IS_ANCILLARY = 0x20000000;
public static final int BIT_CHUNK_IS_PRIVATE = 0x00200000;
public static final int BIT_CHUNK_IS_RESERVED = 0x00002000;
public static final int BIT_CHUNK_IS_SAFE_TO_COPY = 0x00000020;
// public static final byte[] SHIFTS_1 = new byte[] {
// (byte)0x01, // bit 0
// (byte)0x02, // bit 1
// (byte)0x04, // bit 2
// (byte)0x08, // bit 3
// (byte)0x10, // bit 4
// (byte)0x20, // bit 5
// (byte)0x40, // bit 6
// (byte)0x80, // bit 7
// };
//public static final PngChunkCodes Foo = new PngChunkCodes(123, "Foo");
}
@@ -1,125 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng;
/**
* Each "undo*" function reverses the effect of a filter on a given single scanline.
*/
public class PngFilter {
public static void undoSubFilter(byte[] bytes, int pixelStart, int pixelEnd, int filterUnit, byte[] previousRow) {
// int ai = 0;
// for (int i=pixelStart+filterUnit; i<pixelEnd; i++) {
//
// final int x = bytes[i];
// final int a = bytes[pixelStart + ai];
int ai = pixelStart;
for (int i = pixelStart + filterUnit; i < pixelEnd; i++) {
final int x = bytes[i];
final int a = bytes[ai];
//bytes[rowPosition] = (byte)((bytes[rowPosition] + left) & 0xff); // TODO & 0xff
bytes[i] = (byte) ((x + a) & 0xff);
ai++;
}
}
public static void undoUpFilter(byte[] bytes, int pixelStart, int pixelEnd, int filterUnit, byte[] previousRow) {
// for (int i=1; i<bytesPerLine; i++) {
// rowPosition++; // before the first op to skip filter byte
// bytes[rowPosition] = (byte)((bytes[rowPosition] + previousRow[i]) & 0xff);
// }
int bi = 0;
for (int i = pixelStart; i < pixelEnd; i++) {
final int x = bytes[i];
final int b = previousRow[bi];
bytes[i] = (byte) ((x + b) & 0xff);
bi++;
}
}
public static void undoAverageFilter(byte[] bytes, int pixelStart, int pixelEnd, int filterUnit, byte[] previousRow) {
int ai = pixelStart - filterUnit;
int bi = 0;
for (int i = pixelStart; i < pixelEnd; i++) {
final int x = bytes[i];
final int a = (ai < pixelStart) ? 0 : (0xff & bytes[ai]);
final int b = (0xff & previousRow[bi]);
// int ai = pixelStart;
// int bi = 0;
// for (int i=pixelStart+filterUnit; i<pixelEnd; i++) {
// final int x = bytes[i];
// final int a = bytes[ai];
// final int b = previousRow[bi];
//bytes[i] = (byte)((x+((a+b)>>1))&0xff);
//int z = x+(a+b)/2;
int z = x + ((a + b) / 2);
bytes[i] = (byte) (0xff & z);
ai++;
bi++;
}
/*
int bi = 0;
for (int i=pixelStart; i<pixelStart+filterUnit; i++) {
final int x = bytes[i];
final int b = previousRow[bi];
bytes[i] = (byte)((x + (b >> 1)) & 0xff);
bi++;
}
for (int i=pixelStart+filterUnit; i<pixelEnd; i++) {
final int x = bytes[i];
final int a = bytes[pixelStart+bi-filterUnit];
final int b = previousRow[bi];
bytes[i] = (byte)((x + ((a+b) >> 1)) & 0xff);
bi++;
}*/
}
/**
* See http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html
*
*/
public static void undoPaethFilter(byte[] bytes, int pixelStart, int pixelEnd, int filterUnit, byte[] previousRow) {
//int scratch;
//int previousLeft = 0;
//int left = 0;
int ai = pixelStart - filterUnit;
int bi = 0;
int ci = -filterUnit;
//for (int i=0; i<bytesPerLine-1; i++) {
//for (int i=filterUnit; i<bytesPerLine-1; i++) {
for (int i = pixelStart; i < pixelEnd; i++) {
final int a, b, c, x;
x = bytes[i];
if (ai < pixelStart) {
a = c = 0;
} else {
a = 0xff & bytes[ai];
c = 0xff & previousRow[ci];
}
b = 0xff & previousRow[bi];
final int p = a + b - c;
final int pa = p >= a ? p - a : a - p;
final int pb = p >= b ? p - b : b - p;
final int pc = p >= c ? p - c : c - p;
final int predicted = (pa <= pb && pa <= pc) ? a
: (pb <= pc) ? b
: c;
bytes[i] = (byte) ((x + predicted) & 0xff);
ai++;
bi++;
ci++;
}
}
}
@@ -1,166 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
import org.jackhuang.hmcl.ui.image.apng.reader.PngScanlineProcessor;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* A PngScanlineBuffer allows up-front once-off memory allocation that can be used
* across an entire PNG (no matter how many frames) to accumulate and process image data.
*/
public class PngScanlineBuffer {
protected final byte[] bytes;
protected final int filterUnit;
protected byte[] previousLine;
protected int readPosition;
protected int writePosition;
//protected int numBytesPerLine;
public PngScanlineBuffer(int size, int maxBytesPerLine, int filterUnit) {
this.bytes = new byte[size];
this.previousLine = new byte[maxBytesPerLine];
//this.scratchLine = new byte[maxBytesPerLine];
//this.numBytesPerLine = numBytesPerLine;
this.filterUnit = filterUnit;
reset();
}
public byte[] getBytes() {
return bytes;
}
public int getReadPosition() {
return readPosition;
}
public int getWritePosition() {
return writePosition;
}
public boolean canRead(int minNumBytes) {
return availableRead() >= minNumBytes;
}
public int availableRead() {
return writePosition - readPosition;
}
public int availableWrite() {
return bytes.length - writePosition;
}
public void skip(int numBytes) {
if (numBytes < 0 || numBytes > availableRead()) {
throw new IllegalArgumentException(String.format("Skip bytes must be 0 > n >= %d: %d", availableRead(), numBytes));
}
readPosition += numBytes;
}
public void reset() {
this.readPosition = 0;
this.writePosition = 0;
Arrays.fill(previousLine, (byte) 0);
}
public void shift() {
// It is difficult to say whether there is a more efficient approach - e.g. a for loop -
// to shifting the bytes to the start of the array. For now, I consider System.arraycopy
// to be "good enough".
int len = availableRead();
System.arraycopy(bytes, readPosition, bytes, 0, len);
writePosition = len;
readPosition = 0;
}
/**
* Read the decompressed data into the buffer.
*
* @param is an InputStream yielding decompressed data.
* @return count of how many bytes read, or -1 if EOF hit.
* @throws IOException
*/
public int read(InputStream is) throws IOException {
int r = is.read(bytes, writePosition, availableWrite());
if (r > 0) {
writePosition += r;
}
return r;
}
public boolean decompress(InputStream inputStream, PngScanlineProcessor processor) throws IOException, PngException {
int bytesPerLine = processor.getBytesPerLine();
try (InputStream iis = processor.makeInflaterInputStream(inputStream)) {
while (true) {
// Ally says
// I LOVE Y.O.U.
int len = read(iis);
if (len <= 0) {
return processor.isFinished();
}
// Or could do: len -= bytesPerLine until len < bytesPerLine
while (canRead(bytesPerLine)) {
undoFilterScanline(bytesPerLine);
processor.processScanline(bytes, readPosition + 1);//(), getReadPosition());
skip(bytesPerLine);
}
shift();
}
}
}
public void undoFilterScanline(int bytesPerLine) {
int filterCode = bytes[readPosition];
switch (filterCode) {
case 0:
// NOP
break;
case 1:
PngFilter.undoSubFilter(bytes, readPosition + 1, readPosition + bytesPerLine, filterUnit, previousLine);
break;
case 2:
PngFilter.undoUpFilter(bytes, readPosition + 1, readPosition + bytesPerLine, filterUnit, previousLine);
break;
case 3:
PngFilter.undoAverageFilter(bytes, readPosition + 1, readPosition + bytesPerLine, filterUnit, previousLine);
break;
case 4:
PngFilter.undoPaethFilter(bytes, readPosition + 1, readPosition + bytesPerLine, filterUnit, previousLine);
break;
default: // I toyed with an exception here. But why? Just treat as if no filter.
//throw new IllegalArgumentException(String.format("Filter type %d invalid; valid is 0..4", filterCode);
break;
}
// when un-filtering scanlines, the previous row is a copy of the *un-filtered* row (not the original).
System.arraycopy(bytes, readPosition + 1, previousLine, 0, bytesPerLine - 1); // keep a copy of the unmodified row
}
public static PngScanlineBuffer from(PngHeader header) {
return from(header, PngConstants.DEFAULT_TARGET_BUFFER_SIZE);
}
public static PngScanlineBuffer from(PngHeader header, int minBufferSize) {
return new PngScanlineBuffer(sizeFrom(header, minBufferSize), header.bytesPerRow, header.filterOffset);
}
public static int sizeFrom(PngHeader header, int minBufferSize) {
int bytesPerRow = header.bytesPerRow;
int bytesFullBitmap = bytesPerRow * header.height;
if (bytesFullBitmap < minBufferSize) {
return bytesFullBitmap;
}
int numRows = Math.max(1, minBufferSize / bytesPerRow);
return numRows * bytesPerRow;
}
}
@@ -1,48 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.argb8888;
/**
* A bitmap where each pixel is represented by 8-bits for alpha, red, green and blue
* respectively, and each pixel stored in a single 32-bit integer. This is expressly designed
* to be compatible with the input array used to build Android Bitmap objects, though of course
* its use is not limited to that.
*/
public final class Argb8888Bitmap {
public final int[] array;
public final int width;
public final int height;
public Argb8888Bitmap(int width, int height) {
this(new int[width * height], width, height);
}
public Argb8888Bitmap(int[] array, int width, int height) {//}, int x, int y) {
this.array = array;
this.width = width;
this.height = height;
// this.x = y;
}
/**
* Create a new Bitmap that shares the byte array of this bitmap.
* This is useful when rendering a number of animation frames from an APNG file
* and wanting to minimise memory allocation, while still wanting a "new" bitmap
* to work on.
*
* @param width in pixels of "new" bitmap (actual pixel array is the exact array
* from the original bitmap).
* @param height in pixels of "new" bitmap (actual pixel array is the exact array
* from the original bitmap).
* @return new bitmap object sharing the same data array.
*/
public Argb8888Bitmap makeView(int width, int height) {//}, int x, int y) {
if ((width * height) > (this.width * this.height)) {
throw new IllegalArgumentException(String.format(
"Requested width and height (%d x %d) exceeds maximum pixels allowed by host bitmap (%d x %d",
width, height, this.width, this.height));
}
return new Argb8888Bitmap(array, width, height);
}
}
@@ -1,72 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.argb8888;
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.chunks.PngHeader;
import java.util.ArrayList;
import java.util.List;
/**
* An Argb8888BitmapSequence object represents all bitmaps in a single PNG file,
* whether it has one and only one default image, any number of frames in an animation
* or a default image and a separate set of animation frames.
* <p>
* Note that instances of this class will hold an individual bitmap for every frame
* and does <em>not</em> do composition of images in any way. Composition is done in
* the japng_android library using an Android Canvas. This class is not used for that,
* as the intermediate Argb8888Bitmap objects are not required, only one bitmap and
* the output buffer is required during composition.
*/
public final class Argb8888BitmapSequence {
public final PngHeader header;
public final Argb8888Bitmap defaultImage;
private boolean defaultImageIsSet = false;
private PngAnimationControl animationControl;
List<Frame> animationFrames;
public Argb8888BitmapSequence(PngHeader header) {
this.header = header;
this.defaultImage = new Argb8888Bitmap(header.width, header.height);
}
public void receiveAnimationControl(PngAnimationControl animationControl) {
this.animationControl = animationControl;
this.animationFrames = new ArrayList<>(animationControl.numFrames);
}
public void receiveDefaultImage(Argb8888Bitmap bitmap) {
defaultImageIsSet = true;
}
public boolean hasDefaultImage() {
return defaultImageIsSet;
}
public boolean isAnimated() {
return null != animationControl && animationControl.numFrames > 0;
}
public PngAnimationControl getAnimationControl() {
return animationControl;
}
public List<Frame> getAnimationFrames() {
return animationFrames;
}
public static final class Frame {
public final PngFrameControl control;
public final Argb8888Bitmap bitmap;
public Frame(PngFrameControl control, Argb8888Bitmap bitmap) {
this.control = control;
this.bitmap = bitmap;
}
}
}
@@ -1,82 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.argb8888;
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;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
/**
* Argb8888BitmapSequenceDirector instances direct an Argb8888Processor to build all frames
* of an animation into an Argb8888BitmapSequence object.
*/
public class Argb8888BitmapSequenceDirector extends BasicArgb8888Director<Argb8888BitmapSequence> {
Argb8888BitmapSequence bitmapSequence = null;
PngFrameControl currentFrame = null;
private PngHeader header;
@Override
public void receiveHeader(PngHeader header, PngScanlineBuffer buffer) throws PngException {
this.header = header;
this.bitmapSequence = new Argb8888BitmapSequence(header);
//this.header = header;
//defaultImage = new Argb8888Bitmap(header.width, header.height);
this.scanlineProcessor = Argb8888Processors.from(header, buffer, this.bitmapSequence.defaultImage);
}
@Override
public boolean wantDefaultImage() {
return true;
}
@Override
public boolean wantAnimationFrames() {
return true;
}
@Override
public Argb8888ScanlineProcessor beforeDefaultImage() {
return scanlineProcessor;
}
@Override
public void receiveDefaultImage(Argb8888Bitmap bitmap) {
this.bitmapSequence.receiveDefaultImage(bitmap);
}
@Override
public void receiveAnimationControl(PngAnimationControl control) {
this.bitmapSequence.receiveAnimationControl(control);
}
@Override
public Argb8888ScanlineProcessor receiveFrameControl(PngFrameControl control) {
//throw new IllegalStateException("TODO up to here");
//return null;
currentFrame = control;
//System.out.println("Frame: "+control);
return scanlineProcessor.cloneWithNewBitmap(header.adjustFor(control)); // TODO: is this going to be a problem?
}
@Override
public void receiveFrameImage(Argb8888Bitmap bitmap) {
if (null == currentFrame) {
throw new IllegalStateException("Received a frame image with no frame control in place");
}
if (null == bitmapSequence.animationFrames) {
throw new IllegalStateException("Received a frame image without animation control (or frame list?) in place");
}
bitmapSequence.animationFrames.add(new Argb8888BitmapSequence.Frame(currentFrame, bitmap));
currentFrame = null;
}
@Override
public Argb8888BitmapSequence getResult() {
return bitmapSequence;
}
}
@@ -1,47 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.argb8888;
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;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
/**
* Argb8888Director implementations "direct" a given Argb8888Processor how to
* control the output. This allows the Argb8888Processor to transform pixels into
* ARGB8888 format while allowing for radically different final output objects,
* e.g. a single bitmap, a sequence of bitamps, an Android View or Drawable, etc.
* <p>
* TODO: not sure if this will stay in this form. Needs refinement.
*/
public interface Argb8888Director<ResultT> {
void receiveHeader(PngHeader header, PngScanlineBuffer buffer) throws PngException;
void receivePalette(Argb8888Palette palette);
void processTransparentPalette(byte[] bytes, int position, int length) throws PngException;
void processTransparentGreyscale(byte k1, byte k0) throws PngException;
void processTransparentTruecolour(byte r1, byte r0, byte g1, byte g0, byte b1, byte b0) throws PngException;
boolean wantDefaultImage();
boolean wantAnimationFrames();
Argb8888ScanlineProcessor beforeDefaultImage();
void receiveDefaultImage(Argb8888Bitmap bitmap);
void receiveAnimationControl(PngAnimationControl control);
Argb8888ScanlineProcessor receiveFrameControl(PngFrameControl control);
void receiveFrameImage(Argb8888Bitmap bitmap);
ResultT getResult();
}
@@ -1,117 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package 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;
/**
* A palette implementation representing colours in ARGB8888 format as 32-bit integers.
* <p>
* Note that the transparency of individual entries in the palette is modified in-place
* when the trNS chunk is processed.
*
* @see BasicArgb8888Director
*/
public class Argb8888Palette {
protected static Argb8888Palette monochromePalette = null;
protected static Argb8888Palette greyPalette2 = null;
protected static Argb8888Palette greyPalette4 = null;
protected static Argb8888Palette greyPalette8 = null;
/**
* The colour array is public and mutable by design.
*
* @see this.get()
*/
public final int[] argbArray;
/**
* Construct a new palette using the specific array passed as the argument as-is (not copied).
*
* @param argbArray array of colours to use as-is (it is not copied).
*/
public Argb8888Palette(int[] argbArray) {
this.argbArray = argbArray;
}
/**
* Retrieve a colour using a method. Users can also just use the argbArray directly.
*
* @param index of colour to retrieve
* @return integer representing the colour at that index.
*/
public int get(int index) {
return this.argbArray[index];
}
/**
* @return the number of colours in this palette, which is the same thing as the size of the array.
*/
public int size() {
return argbArray.length;
}
/**
* Build a new palette by reading the byte array (from the offset position and for length bytes)
* as provided in a PLTE chunk.
*
* @param bytes array to read data from
* @param position offset into bytes array to begin reading from
* @param length number of bytes to read from bytes array
* @return new palette formed by reading the bytes array.
* @throws PngException
*/
public static Argb8888Palette fromPaletteBytes(byte[] bytes, int position, int length) throws PngException {
int numColours = length / 3; // guaranteed to be divisible by 3
int[] argbArray = new int[numColours];
int srcIndex = position;
int alpha = 0xff << 24;
for (int destIndex = 0; destIndex < numColours; destIndex++) {
final int r = bytes[srcIndex++] & 0xff;
final int g = bytes[srcIndex++] & 0xff;
final int b = bytes[srcIndex++] & 0xff;
argbArray[destIndex] = alpha | r << 16 | g << 8 | b;
}
return new Argb8888Palette(argbArray);
}
public static Argb8888Palette forGreyscale(int numEntries, int step) {
int[] array = new int[numEntries];
int alpha = 0xff << 24;
int grey = 0;
for (int i = 0; i < numEntries; i++) {
array[i] = alpha | grey << 16 | grey << 8 | grey;
grey = (grey + step) & 0xff;
}
return new Argb8888Palette(array);
}
public static Argb8888Palette forGreyscale(int bitDepth) throws PngException {
switch (bitDepth) {
case 1:
if (null == monochromePalette) {
monochromePalette = forGreyscale(2, 0xff); // Worth it or not really?
}
return monochromePalette;
case 2:
if (null == greyPalette2) {
greyPalette2 = forGreyscale(4, 0x55);
}
return greyPalette2;
case 4:
if (null == greyPalette4) {
greyPalette4 = forGreyscale(16, 0x11);
}
return greyPalette4;
case 8: // TODO: need??
if (null == greyPalette8) {
greyPalette8 = forGreyscale(256, 0x01);
}
return greyPalette8;
default:
throw new PngIntegrityException(String.format("Valid greyscale bit depths are 1, 2, 4, 8, not %d", bitDepth));
}
}
}
@@ -1,173 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.argb8888;
import org.jackhuang.hmcl.ui.image.apng.PngChunkCode;
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;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngGamma;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import org.jackhuang.hmcl.ui.image.apng.map.PngChunkMap;
import org.jackhuang.hmcl.ui.image.apng.reader.PngChunkProcessor;
import java.io.IOException;
import java.io.InputStream;
/**
* Concrete implementation of a chunk processor designed to process pixels into
* 32-bit ARGB8888 format.
*/
public class Argb8888Processor<ResultT> implements PngChunkProcessor<ResultT> { //PngChunkProcessor<Argb8888Bitmap> {
protected PngHeader header = null;
protected PngScanlineBuffer scanlineReader = null;
protected Argb8888Director<ResultT> builder = null;
protected Argb8888ScanlineProcessor scanlineProcessor = null;
public Argb8888Processor(Argb8888Director<ResultT> builder) {
this.builder = builder;
}
@Override
public void processHeader(PngHeader header) throws PngException {
// super.processHeader(header);
//// if (header.bitDepth != 1 || header.colourType != PngColourType.PNG_GREYSCALE) {
//// throw new PngFeatureException("ARGB888 only supports 1-bit greyscale");
//// }
// if (header.isGreyscale()) {
// palette = Argb8888Palette.forGreyscale(header.bitDepth);
// }
// scanlineReader = PngScanlineBuffer.from(header);
this.header = header;
this.scanlineReader = PngScanlineBuffer.from(header);
this.builder.receiveHeader(this.header, this.scanlineReader);
}
@Override
public void processGamma(PngGamma gamma) throws PngException {
// No gamma processing is done at the moment.
}
@Override
public void processPalette(byte[] bytes, int position, int length) throws PngException {
//palette = Argb8888Palette.fromPaletteBytes(bytes, position, length);
//scanlineProcessor = Argb8888Processors.fromPalette(this.header, palette);
builder.receivePalette(Argb8888Palette.fromPaletteBytes(bytes, position, length));
}
@Override
public void processTransparency(byte[] bytes, int position, int length) throws PngException {
switch (header.colourType) {
case PNG_GREYSCALE: // colour type 0
// grey sample value (2 bytes)
if (length != 2) {
throw new PngIntegrityException(String.format("tRNS chunk for greyscale image must be exactly length=2, not %d", length));
}
builder.processTransparentGreyscale(bytes[0], bytes[1]);
break;
case PNG_TRUECOLOUR: // colour type 2
// red, green, blue samples, EACH with two bytes (16-bits)
builder.processTransparentTruecolour(bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]);
break;
case PNG_INDEXED_COLOUR: // colour type 3
// This is a sequence of one-byte alpha values to apply to each palette entry starting at zero.
// The number of entries may be less than the size of the palette, but not more.
builder.processTransparentPalette(bytes, position, length);
break;
case PNG_GREYSCALE_WITH_ALPHA:
case PNG_TRUECOLOUR_WITH_ALPHA:
default:
throw new PngIntegrityException("Illegal to have tRNS chunk with image type " + header.colourType.name);
}
}
/**
* The only supported animation type is not "NOT_ANIMATED".
*/
// @Override
// public PngAnimationType chooseApngImageType(PngAnimationType type, PngFrameControl currentFrame) throws PngException {
// scanlineProcessor = Argb8888ScanlineProcessor.from(header, scanlineReader, currentFrame);
// return PngAnimationType.NOT_ANIMATED;
// }
@Override
public void processDefaultImageData(InputStream inputStream, PngChunkCode code, int position, int length) throws IOException, PngException {
if (!builder.wantDefaultImage()) {
inputStream.skip(length); // important!
return;
}
if (null == scanlineProcessor) { // TODO: is that a good enough metric? Or could be numIdat==0?
scanlineProcessor = builder.beforeDefaultImage();
if (null == scanlineProcessor) throw new IllegalStateException("Builder must create scanline processor");
}
if (scanlineReader.decompress(inputStream, scanlineProcessor)) {
// If here, the image is fully decompressed.
builder.receiveDefaultImage(scanlineProcessor.getBitmap());
scanlineProcessor = null;
scanlineReader.reset();
}
}
@Override
public void processAnimationControl(PngAnimationControl animationControl) throws PngException {
if (builder.wantAnimationFrames()) {
builder.receiveAnimationControl(animationControl);
}
}
@Override
public void processFrameControl(PngFrameControl frameControl) throws PngException {
if (!builder.wantAnimationFrames()) {
return;
}
if (null == scanlineProcessor) {
// If here, the data in this frame image has not started
scanlineProcessor = builder.receiveFrameControl(frameControl);
if (null == scanlineProcessor)
throw new IllegalStateException("Builder must create scanline processor for frame");
} else {
throw new IllegalStateException("received animation frame control but image data was in progress");
}
}
@Override
public void processFrameImageData(InputStream inputStream, PngChunkCode code, int position, int length) throws IOException, PngException {
//throw new PngFeatureException("PngArgb8888Processor does not support animation frames");
if (!builder.wantAnimationFrames()) {
inputStream.skip(length);
return;
}
if (null == scanlineProcessor) {
throw new IllegalStateException("received animation frame image data before frame control or without processor in place");
}
if (scanlineReader.decompress(inputStream, scanlineProcessor)) {
// If here, the image is fully decompressed.
builder.receiveFrameImage(scanlineProcessor.getBitmap());
scanlineReader.reset();
scanlineProcessor = null;
}
}
@Override
public void processChunkMapItem(PngChunkMap chunkMapItem) throws PngException {
// NOP
}
@Override
public ResultT getResult() {
//return scanlineProcessor.getBitmap();
return builder.getResult();
}
}
@@ -1,462 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.argb8888;
import org.jackhuang.hmcl.ui.image.apng.PngConstants;
import org.jackhuang.hmcl.ui.image.apng.PngScanlineBuffer;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngFeatureException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
/**
* A series of scanline processor implementations for different input pixel formats,
* each able to transform the input into ARGB8888 output pixels.
*/
public class Argb8888Processors {
/**
* Determine from the header the concrete Argb8888Processor implementation the
* caller needs to transform the incoming pixels into the ARGB8888 pixel format.
*
* @param header of the image being loaded
* @param scanlineReader the scanline buffer being used
* @param bitmap the destination bitmap
* @return a concrete Argb8888Processor to transform source pixels into the specific bitmap.
* @throws PngException if there is a feature not supported or some specification break with the file.
*/
public static Argb8888ScanlineProcessor from(PngHeader header, PngScanlineBuffer scanlineReader, Argb8888Bitmap bitmap) throws PngException {
int bytesPerScanline = header.bytesPerRow;
switch (header.colourType) {
case PNG_GREYSCALE:
switch (header.bitDepth) {
case 1:
return new IndexedColourBits(bytesPerScanline, bitmap, 7, 0x01, PngConstants.SHIFTS_1, Argb8888Palette.forGreyscale(1));
case 2:
return new IndexedColourBits(bytesPerScanline, bitmap, 3, 0x03, PngConstants.SHIFTS_2, Argb8888Palette.forGreyscale(2));
case 4:
return new IndexedColourBits(bytesPerScanline, bitmap, 1, 0x0F, PngConstants.SHIFTS_4, Argb8888Palette.forGreyscale(4));
case 8:
return new Greyscale8(bytesPerScanline, bitmap);
case 16:
throw new PngFeatureException("Greyscale supports 1, 2, 4, 8 but not 16.");
default:
throw new PngIntegrityException(String.format("Invalid greyscale bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
}
case PNG_GREYSCALE_WITH_ALPHA:
switch (header.bitDepth) {
case 4:
return new Greyscale4Alpha(bytesPerScanline, bitmap);
case 8:
return new Greyscale8Alpha(bytesPerScanline, bitmap);
case 16:
return new Greyscale16Alpha(bytesPerScanline, bitmap);
default:
throw new PngIntegrityException(String.format("Invalid greyscale-with-alpha bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
}
case PNG_INDEXED_COLOUR:
switch (header.bitDepth) {
case 1:
return new IndexedColourBits(bytesPerScanline, bitmap, 7, 0x01, PngConstants.SHIFTS_1);
case 2:
return new IndexedColourBits(bytesPerScanline, bitmap, 3, 0x03, PngConstants.SHIFTS_2);
case 4:
return new IndexedColourBits(bytesPerScanline, bitmap, 1, 0x0F, PngConstants.SHIFTS_4);
case 8:
return new IndexedColour8(bytesPerScanline, bitmap);
default:
throw new PngIntegrityException(String.format("Invalid indexed colour bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
}
case PNG_TRUECOLOUR:
switch (header.bitDepth) {
case 8:
return new Truecolour8(bytesPerScanline, bitmap);
case 16:
return new Truecolour16(bytesPerScanline, bitmap);
default:
throw new PngIntegrityException(String.format("Invalid truecolour bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
}
case PNG_TRUECOLOUR_WITH_ALPHA:
switch (header.bitDepth) {
case 8:
return new Truecolour8Alpha(bytesPerScanline, bitmap);
case 16:
return new Truecolour16Alpha(bytesPerScanline, bitmap);
default:
throw new PngIntegrityException(String.format("Invalid truecolour with alpha bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
}
default:
throw new PngFeatureException("ARGB8888 doesn't support PNG mode " + header.colourType.name());
}
}
/**
* Transforms 1-, 2-, 4-bit indexed colour source pixels to ARGB8888 pixels.
*/
public static class IndexedColourBits extends Argb8888ScanlineProcessor {
private int highBit;
private int mask;
private byte[] shifts;
public IndexedColourBits(int bytesPerScanline, Argb8888Bitmap bitmap, int highBit, int mask, byte[] shifts) {
this(bytesPerScanline, bitmap, highBit, mask, shifts, null);
}
public IndexedColourBits(int bytesPerScanline, Argb8888Bitmap bitmap, int highBit, int mask, byte[] shifts, Argb8888Palette palette) {
super(bytesPerScanline, bitmap);
this.highBit = highBit;
this.mask = mask;
this.shifts = shifts;
this.palette = palette;
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
int writePosition = this.y * width;
int lastWritePosition = writePosition + width;
int bit = highBit;
while (writePosition < lastWritePosition) {
//final int index=(srcBytes[srcPosition+x] & masks[bit]) >> shifts[bit];
//final int index = (srcBytes[srcPosition])
//final int v = srcBytes[srcPosition];
final int index = mask & (srcBytes[srcPosition] >> shifts[bit]);
int dest;
if (palette != null) {
dest = palette.argbArray[index];
} else {
dest = 0;
}
destArray[writePosition++] = dest;
if (bit == 0) {
srcPosition++;
bit = highBit;
} else {
bit--;
}
}
this.y++;
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new IndexedColourBits(bytesPerRow, bitmap, highBit, mask, shifts, palette);
}
}
/**
* Special case implementation to transform 8-bit indexed colour source pixels to ARGB8888 pixels.
*/
public static class IndexedColour8 extends Argb8888ScanlineProcessor {
public IndexedColour8(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline, bitmap);
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
final int index = 0xff & srcBytes[srcPosition++]; // TODO: need to use transparency and background chunks
int dest;
if (palette != null) {
dest = palette.argbArray[index];
} else {
dest = 0;
}
destArray[writePosition++] = dest;
}
this.y++;
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new IndexedColour8(bytesPerRow, bitmap);
}
}
/**
* Transforms 4-bit greyscale with alpha source pixels to ARGB8888 pixels.
*/
public static class Greyscale4Alpha extends Argb8888ScanlineProcessor {
public Greyscale4Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline, bitmap);
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
final int v = srcBytes[srcPosition++];
final int k = PngConstants.GREY_PALETTE_16[0x0f & (v >> 4)];
final int a = PngConstants.GREY_PALETTE_16[0x0f & v];
final int c = a << 24 | k << 16 | k << 8 | k;
destArray[writePosition++] = c;
}
this.y++;
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new Greyscale4Alpha(bytesPerRow, bitmap);
}
}
/**
* Transforms 8-bit greyscale source pixels to ARGB8888 pixels.
*/
public static class Greyscale8 extends Argb8888ScanlineProcessor {
boolean haveTransparent = false;
int transparentSample = 0;
public Greyscale8(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline, bitmap);
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
final int sample = 0xff & srcBytes[srcPosition++];
final int k = (haveTransparent && sample == transparentSample) ? transparentSample : sample;
final int c = alpha | k << 16 | k << 8 | k;
destArray[writePosition++] = c;
}
this.y++;
}
@Override
public void processTransparentGreyscale(byte k1, byte k0) {
// According to below, when image is less than 16-bits per pixel, use least significant byte
// http://www.w3.org/TR/PNG/#11transinfo
haveTransparent = true;
transparentSample = 0xff & k0; // NOT k1 according to http://www.w3.org/TR/PNG/#11transinfo
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new Greyscale8(bytesPerRow, bitmap);
}
}
/**
* Transforms 8-bit greyscale with alpha source pixels to ARGB8888 pixels.
*/
public static class Greyscale8Alpha extends Argb8888ScanlineProcessor {
public Greyscale8Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline, bitmap);
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
final int k = 0xff & srcBytes[srcPosition++];
final int a = 0xff & srcBytes[srcPosition++];
final int c = a << 24 | k << 16 | k << 8 | k;
destArray[writePosition++] = c;
}
this.y++;
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new Greyscale8Alpha(bytesPerRow, bitmap);
}
}
/**
* Transforms 16-bit greyscale with alpha source pixels to ARGB8888 pixels.
*/
public static class Greyscale16Alpha extends Argb8888ScanlineProcessor {
public Greyscale16Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline, bitmap);
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
final int k = 0xff & srcBytes[srcPosition++];
srcPosition++; // skip the least-significant byte of the grey
final int a = 0xff & srcBytes[srcPosition++];
srcPosition++; // skip the least-significant byte of the alpha
final int c = a << 24 | k << 16 | k << 8 | k;
destArray[writePosition++] = c;
}
this.y++;
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new Greyscale16Alpha(bytesPerRow, bitmap);
}
}
/**
* Transforms true-colour (RGB) 8-bit source pixels to ARGB8888 pixels.
*/
public static class Truecolour8 extends Argb8888ScanlineProcessor {
public Truecolour8(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline, bitmap);
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
final int r = 0xff & srcBytes[srcPosition++];
final int g = 0xff & srcBytes[srcPosition++];
final int b = 0xff & srcBytes[srcPosition++];
final int c = alpha | r << 16 | g << 8 | b;
destArray[writePosition++] = c;
}
this.y++;
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new Truecolour8(bytesPerRow, bitmap);
}
}
/**
* Transforms true-colour with alpha (RGBA) 8-bit source pixels to ARGB8888 pixels.
*/
public static class Truecolour8Alpha extends Argb8888ScanlineProcessor {
public Truecolour8Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline, bitmap);
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
int writePosition = this.y * width;
//srcPosition++; // skip filter byte
for (int x = 0; x < width; x++) {
final int r = 0xff & srcBytes[srcPosition++];
final int g = 0xff & srcBytes[srcPosition++];
final int b = 0xff & srcBytes[srcPosition++];
final int a = 0xff & srcBytes[srcPosition++];
final int c = a << 24 | r << 16 | g << 8 | b;
destArray[writePosition++] = c;
}
this.y++;
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new Truecolour8Alpha(bytesPerRow, bitmap);
}
}
/**
* Transforms true-colour (RGB) 16-bit source pixels to ARGB8888 pixels.
*/
public static class Truecolour16 extends Argb8888ScanlineProcessor {
public Truecolour16(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline, bitmap);
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
int writePosition = this.y * width;
//srcPosition++; // skip filter byte
for (int x = 0; x < width; x++) {
final int r = 0xff & srcBytes[srcPosition];
srcPosition += 2; // skip the byte just read and the least significant byte of the next
final int g = 0xff & srcBytes[srcPosition];
srcPosition += 2; // ditto
final int b = 0xff & srcBytes[srcPosition];
srcPosition += 2; // ditto again
final int c = alpha | r << 16 | g << 8 | b;
destArray[writePosition++] = c;
}
this.y++;
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new Truecolour16(bytesPerRow, bitmap);
}
}
/**
* Transforms true-colour with alpha (RGBA) 16-bit source pixels to ARGB8888 pixels.
* <p>
* Note that the simpler method of resampling the colour is done, namely discard the LSB.
*/
public static class Truecolour16Alpha extends Argb8888ScanlineProcessor {
public Truecolour16Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline, bitmap);
}
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
//final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
int writePosition = this.y * width;
//srcPosition++; // skip filter byte
for (int x = 0; x < width; x++) {
final int r = 0xff & srcBytes[srcPosition];
srcPosition += 2; // skip the byte just read and the least significant byte of the next
final int g = 0xff & srcBytes[srcPosition];
srcPosition += 2; // ditto
final int b = 0xff & srcBytes[srcPosition];
srcPosition += 2; // ditto again
final int alpha = 0xff & srcBytes[srcPosition];
srcPosition += 2; // skip the byte just read and the least significant byte of the next
final int c = alpha << 24 | r << 16 | g << 8 | b;
destArray[writePosition++] = c;
}
this.y++;
}
@Override
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
return new Truecolour16Alpha(bytesPerRow, bitmap);
}
}
private Argb8888Processors() {
}
}
@@ -1,84 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.argb8888;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
import org.jackhuang.hmcl.ui.image.apng.reader.BasicScanlineProcessor;
/**
* Base implementation that transforms scanlines of source pixels into scanlines of
* ARGB8888 (32-bit integer) pixels in a destination Argb8888Bitmap object.
* <p>
* Note: I wonder if a better name is ScanlineConverter or PixelTransformer.
*
* @see Argb8888Processors
*/
public abstract class Argb8888ScanlineProcessor extends BasicScanlineProcessor {
protected Argb8888Bitmap bitmap;
protected Argb8888Palette palette;
protected int y = 0;
// public Argb8888ScanlineProcessor(PngHeader header) {
// this(header.bytesPerRow, new Argb8888Bitmap(header.width, header.height));
// }
public Argb8888ScanlineProcessor(int bytesPerScanline, Argb8888Bitmap bitmap) {
super(bytesPerScanline);
this.bitmap = bitmap;
}
// public Argb8888ScanlineProcessor(PngHeader header, PngScanlineBuffer scanlineReader, PngFrameControl currentFrame) {
// super(header, scanlineReader, currentFrame);
// bitmap = new Argb8888Bitmap(this.header.width, this.header.height);
// }
public Argb8888Bitmap getBitmap() {
return bitmap;
}
public Argb8888Palette getPalette() {
return palette;
}
public void setPalette(Argb8888Palette palette) {
this.palette = palette;
}
/**
* When processing a sequence of frames a caller may want to write to a completely
* new bitmap for every frame or re-use the bytes in an existing bitmap.
*
* @param header of image to use as basis for calculating image size.
* @return processor that will write to a new bitmap.
*/
public Argb8888ScanlineProcessor cloneWithNewBitmap(PngHeader header) {
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, new Argb8888Bitmap(header.width, header.height));
if (this.palette != null) {
cloned.setPalette(new Argb8888Palette(palette.argbArray));
}
return cloned;
}
/**
* When processing a sequence of frames a caller may want to write to a completely
* new bitmap for every frame or re-use the bytes in an existing bitmap.
*
* @param header of image to use as basis for calculating image size.
* @return processor that will write to the same bitmap as the current processor.
*/
public Argb8888ScanlineProcessor cloneWithSharedBitmap(PngHeader header) {
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, bitmap.makeView(header.width, header.height));
if (this.palette != null) {
cloned.setPalette(new Argb8888Palette(palette.argbArray));
}
return cloned;
}
// public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
// throw new IllegalStateException("TODO: override");
// }
abstract Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap);
}
@@ -1,45 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package 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;
/**
* Common functionality for Argb8888Director implementations.
*/
public abstract class BasicArgb8888Director<ResultT> implements Argb8888Director<ResultT> {
protected Argb8888ScanlineProcessor scanlineProcessor;
@Override
public void receivePalette(Argb8888Palette palette) {
scanlineProcessor.setPalette(palette);
}
@Override
public void processTransparentPalette(byte[] bytes, int position, int length) throws PngException {
Argb8888Palette palette = scanlineProcessor.getPalette();
if (null == palette) {
throw new PngIntegrityException("Received tRNS data but no palette is in place");
}
if (length <= 0 || length > palette.size()) {
throw new PngIntegrityException(String.format("Received tRNS data length is invalid. Should be >1 && < %d but is %d", palette.size(), length));
}
for (int i = 0; i < length; i++) {
final int alpha = 0xff & bytes[position + i];
palette.argbArray[i] = alpha << 24 | palette.argbArray[i] & 0x00FFFFFF;
}
}
@Override
public void processTransparentGreyscale(byte k1, byte k0) throws PngException {
scanlineProcessor.processTransparentGreyscale(k1, k0);
}
@Override
public void processTransparentTruecolour(byte r1, byte r0, byte g1, byte g0, byte b1, byte b0) throws PngException {
scanlineProcessor.processTransparentTruecolour(r1, r0, g1, g0, b1, b0);
}
}
@@ -1,65 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.argb8888;
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;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
/**
* This will build a single bitmap: the default image within the PNG file.
* Any animation data (or any other data) will not be processed.
*/
public class DefaultImageArgb8888Director extends BasicArgb8888Director<Argb8888Bitmap> {
protected Argb8888Bitmap defaultImage;
@Override
public void receiveHeader(PngHeader header, PngScanlineBuffer buffer) throws PngException {
defaultImage = new Argb8888Bitmap(header.width, header.height);
scanlineProcessor = Argb8888Processors.from(header, buffer, defaultImage);
}
@Override
public boolean wantDefaultImage() {
return true;
}
@Override
public boolean wantAnimationFrames() {
return false;
}
@Override
public Argb8888ScanlineProcessor beforeDefaultImage() {
return scanlineProcessor;
}
@Override
public void receiveDefaultImage(Argb8888Bitmap bitmap) {
}
@Override
public void receiveAnimationControl(PngAnimationControl control) {
}
@Override
public Argb8888ScanlineProcessor receiveFrameControl(PngFrameControl control) {
return null;
}
@Override
public void receiveFrameImage(Argb8888Bitmap bitmap) {
}
@Override
public Argb8888Bitmap getResult() {
return defaultImage;
}
}
@@ -1,30 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.chunks;
/**
* A PngAnimationControl object contains data parsed from the ``acTL``
* animation control chunk of an animated PNG file.
*/
public class PngAnimationControl {
public final int numFrames;
public final int numPlays;
public PngAnimationControl(int numFrames, int numPlays) {
this.numFrames = numFrames;
this.numPlays = numPlays;
}
public boolean loopForever() {
return 0 == numPlays;
}
@Override
public String toString() {
return "PngAnimationControl{" +
"numFrames=" + numFrames +
", numPlays=" + numPlays +
'}';
}
}
@@ -1,137 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.chunks;
import org.jackhuang.hmcl.ui.image.apng.map.PngChunkMap;
import java.util.ArrayList;
import java.util.List;
/**
* A PngFrameControl object contains data parsed from the ``fcTL`` chunk data
* in an animated PNG File.
* <p>
* See https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
* <pre>
* 0 sequence_number (unsigned int) Sequence number of the animation chunk, starting from 0
* 4 width (unsigned int) Width of the following frame
* 8 height (unsigned int) Height of the following frame
* 12 x_offset (unsigned int) X position at which to render the following frame
* 16 y_offset (unsigned int) Y position at which to render the following frame
* 20 delay_num (unsigned short) Frame delay fraction numerator
* 22 delay_den (unsigned short) Frame delay fraction denominator
* 24 dispose_op (byte) Type of frame area disposal to be done after rendering this frame
* 25 blend_op (byte) Type of frame area rendering for this frame
* </pre>
* <p>
* Delay denominator: from spec, "if denominator is zero it should be treated as 100ths of second".
* <pre>
* dispose op:
* value
* 0 APNG_DISPOSE_OP_NONE
* 1 APNG_DISPOSE_OP_BACKGROUND
* 2 APNG_DISPOSE_OP_PREVIOUS
*
* blend op:
* value
* 0 APNG_BLEND_OP_SOURCE
* 1 APNG_BLEND_OP_OVER
* </pre>
*/
public class PngFrameControl {
public final int sequenceNumber;
public final int width;
public final int height;
public final int xOffset;
public final int yOffset;
public final short delayNumerator;
public final short delayDenominator;
public final byte disposeOp;
public final byte blendOp;
List<PngChunkMap> imageChunks = new ArrayList<>(1); // TODO: this may be removed
public PngFrameControl(int sequenceNumber, int width, int height, int xOffset, int yOffset, short delayNumerator, short delayDenominator, byte disposeOp, byte blendOp) {
this.sequenceNumber = sequenceNumber;
this.width = width;
this.height = height;
this.xOffset = xOffset;
this.yOffset = yOffset;
this.delayNumerator = delayNumerator;
this.delayDenominator = delayDenominator == 0 ? 100 : delayDenominator; // APNG spec says zero === 100.
this.disposeOp = disposeOp;
this.blendOp = blendOp;
}
/**
* @return number of milliseconds to show this frame for
*/
public int getDelayMilliseconds() {
if (delayDenominator == 1000) {
return delayNumerator;
} else {
// if denom is 100 then need to multiple by 10
float f = 1000 / delayDenominator; // 1000/100 -> 10
return (int) (delayNumerator * f);
}
}
// TODO: can this be removed?
public void appendImageData(PngChunkMap chunkMap) {
imageChunks.add(chunkMap);
}
// TODO: this may be removed
public List<PngChunkMap> getImageChunks() {
return imageChunks;
}
@Override
public String toString() {
return "PngFrameControl{" +
"sequenceNumber=" + sequenceNumber +
", width=" + width +
", height=" + height +
", xOffset=" + xOffset +
", yOffset=" + yOffset +
", delayNumerator=" + delayNumerator +
", delayDenominator=" + delayDenominator +
", disposeOp=" + disposeOp +
", blendOp=" + blendOp +
'}';
}
// mainly for ease in unit testing
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PngFrameControl that = (PngFrameControl) o;
if (sequenceNumber != that.sequenceNumber) return false;
if (width != that.width) return false;
if (height != that.height) return false;
if (xOffset != that.xOffset) return false;
if (yOffset != that.yOffset) return false;
if (delayNumerator != that.delayNumerator) return false;
if (delayDenominator != that.delayDenominator) return false;
if (disposeOp != that.disposeOp) return false;
return blendOp == that.blendOp;
}
@Override
public int hashCode() {
int result = sequenceNumber;
result = 31 * result + width;
result = 31 * result + height;
result = 31 * result + xOffset;
result = 31 * result + yOffset;
result = 31 * result + (int) delayNumerator;
result = 31 * result + (int) delayDenominator;
result = 31 * result + (int) disposeOp;
result = 31 * result + (int) blendOp;
return result;
}
}
@@ -1,22 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.chunks;
import java.io.DataInputStream;
import java.io.IOException;
/**
* A PngGamma object represents data parsed from a ``gAMA`` chunk.
*/
public class PngGamma {
public final int imageGamma;
public PngGamma(int imageGamma) {
this.imageGamma = imageGamma;
}
public static PngGamma from(DataInputStream dis) throws IOException {
return new PngGamma(dis.readInt());
}
}
@@ -1,224 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.chunks;
import org.jackhuang.hmcl.ui.image.apng.PngColourType;
import org.jackhuang.hmcl.ui.image.apng.reader.PngReadHelper;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngFeatureException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import java.io.DataInput;
import java.io.IOException;
/**
* Created by aellerton on 10/05/2015.
*/
public class PngHeader {
/**
* Number of pixels (columns) wide.
*/
public final int width;
/**
* Number of pixels (rows) high.
*/
public final int height;
/**
* The bitDepth is the number of bits for each <em>channel</em> of a given pixel.
* A better name might be "bitsPerPixelChannel" but the name "bitDepth" is used
* throughout the PNG specification.
* <p>
* A truecolour image with a bitDepth of 8 means that the red channel of a pixel
* has 8 bits (so 256 levels of red), green has 8 bits (256 levels of green), and
* blue has 8 bits (so 256 levels of green). That means the total bitsPerPixel
* for that bitmap will be 8+8+8 = 24.
* <p>
* A truecolour <em>with alpha</em> image with bitDepth of 8 will be the same
* except every alpha element of every pixel will have 8 bits (so 256 levels of
* alpha transparency), meaning that the total bitsPerPixel for that bitmap will
* be 8+8+8+8=32.
* <p>
* A truecolour with alpha image with <em>bitDepth of 16</em> means that each of
* red, green blue and alpha have 16-bits respectively, meaning that the total
* bitsPerPixel will be 16+16+16+16 = 64.
* <p>
* A greyscale image (no alpha) with bitDepth of 16 has only a grey channel for
* each pixel, so the bitsPerPixel will also be 16.
* <p>
* But a greyscale image <em>with alpha</em> with a bitDepth of 16 has a grey
* channel and an alpha channel, each with 16 bits so the bitsPerPixel will be
* 16+16=32.
* <p>
* As for palette-based images...
* <ul>
* <li>A monochrome image or image with 2 colour palette has bitDepth=1.</li>
* <li>An image with 4 colour palette has bitDepth=2.</li>
* <li>An image with 8 colour palette has bitDepth=3.</li>
* <li>An image with 16 colour palette has bitDepth=4.</li>
* <li>A greyscale image with 16 levels of gray <em>and an alpha channel</em>
* has bitDepth=4 and bitsPerPixel=8 because the gray and the alpha channel
* each have 4 bits.</li>
* </ul>
*
* @see #bitsPerPixel
*/
public final byte bitDepth;
/**
* Every PNG image must be exactly one of the standard types as defined by the
* PNG specification. Better names might have been "imageType" or "imageFormat"
* but the name "colourType" is used throughout the PNG spec.
*/
public final PngColourType colourType;
/**
* Compression type of the file.
* In practice this is redundant: it may be zip and nothing else.
*/
public final byte compressionMethod;
/**
* Filter method used by the file.
* In practice this is redundant because the filter types are set in the
* specification and have never been (and never will be) extended.
*/
public final byte filterMethod;
/**
* An image is either interlaced or not interlaced.
* At the time of writing only non-interlaced is supported by this library.
*/
public final byte interlaceMethod;
/**
* The number of bits that comprise a single pixel in this bitmap (or every
* frame if animated). This is distinct from bitDepth.
*
* @see #bitDepth
*/
public final int bitsPerPixel;
public final int bytesPerRow;
public final int filterOffset;
public PngHeader(int width, int height, byte bitDepth, PngColourType colourType) {
this(width, height, bitDepth, colourType, (byte) 0, (byte) 0, (byte) 0);
}
public PngHeader(int width, int height, byte bitDepth, PngColourType colourType, byte compressionMethod, byte filterMethod, byte interlaceMethod) {
this.width = width;
this.height = height;
this.bitDepth = bitDepth;
this.colourType = colourType;
this.compressionMethod = compressionMethod;
this.filterMethod = filterMethod;
this.interlaceMethod = interlaceMethod;
this.bitsPerPixel = bitDepth * colourType.componentsPerPixel;
this.bytesPerRow = PngReadHelper.calculateBytesPerRow(width, bitDepth, colourType, interlaceMethod);
//this.filterOffset = this.bitsPerPixel < 8 ? 1 : this.bitsPerPixel>>3; // minimum of 1 byte. RGB888 will be 3 bytes. RGBAFFFF is 8 bytes.
this.filterOffset = (this.bitsPerPixel + 7) >> 3; // libpng
// from pypng
// # Derived values
// # http://www.w3.org/TR/PNG/#6Colour-values
// colormap = bool(self.color_type & 1)
// greyscale = not (self.color_type & 2)
// alpha = bool(self.color_type & 4)
// color_planes = (3,1)[greyscale or colormap]
// planes = color_planes + alpha
}
@Override
public String toString() {
return "PngHeader{" +
"width=" + width +
", height=" + height +
", bitDepth=" + bitDepth +
", colourType=" + colourType +
", compressionMethod=" + compressionMethod +
", filterMethod=" + filterMethod +
", interlaceMethod=" + interlaceMethod +
'}';
}
public boolean isInterlaced() {
return interlaceMethod == 1;
}
public boolean isZipCompression() {
return compressionMethod == 0;
}
public boolean isGreyscale() {
return colourType == PngColourType.PNG_GREYSCALE | colourType == PngColourType.PNG_GREYSCALE_WITH_ALPHA;
}
/**
* @return true if the image type indicates there is an alpha value for every pixel.
* Note that this take into account any transparency or background chunk.
*/
public boolean hasAlphaChannel() {
return colourType == PngColourType.PNG_GREYSCALE_WITH_ALPHA | colourType == PngColourType.PNG_TRUECOLOUR_WITH_ALPHA;
}
public static PngHeader makeTruecolour(int width, int height) {
return new PngHeader(width, height, (byte) 8, PngColourType.PNG_TRUECOLOUR);
}
public static PngHeader makeTruecolourAlpha(int width, int height) {
return new PngHeader(width, height, (byte) 8, PngColourType.PNG_TRUECOLOUR_WITH_ALPHA);
}
public PngHeader adjustFor(PngFrameControl frame) {
if (frame == null) {
return this;
} else {
return new PngHeader(frame.width, frame.height, this.bitDepth, this.colourType, this.compressionMethod, this.filterMethod, this.interlaceMethod);
}
}
public static void checkHeaderParameters(int width, int height, byte bitDepth, PngColourType colourType, byte compressionMethod, byte filterMethod, byte interlaceMethod) throws PngException {
switch (bitDepth) {
case 1:
case 2:
case 4:
case 8:
case 16:
break; // all fine
default:
throw new PngIntegrityException("Invalid bit depth " + bitDepth);
}
// thanks to pypng
if (colourType.isIndexed() && bitDepth > 8) {
throw new PngIntegrityException(String.format(
"Indexed images (colour type %d) cannot have bitdepth > 8 (bit depth %d)." +
" See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 .", colourType.code, bitDepth));
}
if (bitDepth < 8 && !colourType.supportsSubByteDepth()) {
throw new PngIntegrityException(String.format(
"Illegal combination of bit depth (%d) and colour type (%d)." +
" See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 .", colourType.code, bitDepth));
}
if (interlaceMethod != 0) {
throw new PngFeatureException("Interlaced images are not yet supported");
}
}
public static PngHeader from(DataInput dis) throws IOException, PngException {
int width = dis.readInt();
int height = dis.readInt();
byte bitDepth = dis.readByte();
PngColourType colourType = PngColourType.fromByte(dis.readByte());
byte compressionMethod = dis.readByte();
byte filterMethod = dis.readByte();
byte interlaceMethod = dis.readByte();
checkHeaderParameters(width, height, bitDepth, colourType, compressionMethod, filterMethod, interlaceMethod);
return new PngHeader(width, height, bitDepth, colourType, compressionMethod, filterMethod, interlaceMethod);
}
}
@@ -1,61 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.chunks;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Palette;
import java.util.Arrays;
/**
* A PngPalette object represents an ordered array of RGB (888) colour tuples
* derived from a PLTE chunk.
* <p>
* WARNING: this class may not remain in the API.
* When implementing the Argb8888 decoders it seems clear that every output
* format will benefit from a specific palette implementation, so this attempt
* at a generic palette may be removed.
*
* @see Argb8888Palette
*/
public class PngPalette {
// TODO: should include alpha here? Can then store as int32s?
public final byte[] rgb888;
public final int[] rgba8888; // Including this duplicate for now. Not sure if will keep it.
public final int numColours;
public static final int LENGTH_RGB_BYTES = 3;
public static final int BYTE_INITIAL_ALPHA = 0xff;
public PngPalette(byte[] rgb888, int[] rgba8888) {
this.rgb888 = rgb888;
this.rgba8888 = rgba8888;
this.numColours = rgb888.length / 3;
}
public static PngPalette from(byte[] source, int first, int length) throws PngException {
if (length % LENGTH_RGB_BYTES != 0) {
throw new PngIntegrityException(String.format("Invalid palette data length: %d (not a multiple of 3)", length));
}
return new PngPalette(
Arrays.copyOfRange(source, first, first + length),
rgba8888From(source, first, length)
);
}
private static int[] rgba8888From(byte[] source, int first, int length) {
int last = first + length;
int numColours = length / 3;
int[] rgba8888 = new int[numColours];
int j = 0;
for (int i = first; i < last; i += LENGTH_RGB_BYTES) {
rgba8888[j] = source[i] << 24 | source[i + 1] << 16 | source[i + 2] << 8 | BYTE_INITIAL_ALPHA;
j++;
}
return rgba8888;
}
}
@@ -1,21 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.error;
/**
* All exceptions in the library are a PngException or subclass of it.
*/
public class PngException extends Exception {
int code;
public PngException(int code, String message) {
super(message);
this.code = code;
}
public PngException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}
@@ -1,17 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.error;
import org.jackhuang.hmcl.ui.image.apng.PngConstants;
/**
* A PngFeatureException is thrown when a feature used in a specific file
* is not supported by the pipeline being used. For example, at the time
* of writing, interlaced image reading is not supported.
*/
public class PngFeatureException extends PngException {
public PngFeatureException(String message) {
super(PngConstants.ERROR_FEATURE_NOT_SUPPORTED, message);
}
}
@@ -1,19 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.error;
import org.jackhuang.hmcl.ui.image.apng.PngConstants;
/**
* A PngIntegrityException is thrown when some aspect of the PNG file being
* loaded is invalid or unacceptable according to the PNG Specification.
* <p>
* For example, requesting a 16-bit palette image is invalid, or a colour type
* outside of the allowed set.
*/
public class PngIntegrityException extends PngException {
public PngIntegrityException(String message) {
super(PngConstants.ERROR_INTEGRITY, message);
}
}
@@ -1,52 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.map;
import org.jackhuang.hmcl.ui.image.apng.PngChunkCode;
/**
* A single chunk from a PNG file is represented by a PngChunkMap.
* <p>
* WARNING: not sure if this API will remain.
* </p>
*/
public class PngChunkMap {
/**
* The code like IHDR, IPAL, IDAT. If the chunk code is non-standard,
* the UNKNOWN code will be used and the codeString will be set.
*/
public PngChunkCode code;
/**
* Number of bytes containing the data portion of the chunk. Note that this excludes
* the last 4 bytes that are the CRC checksom.
*/
public int dataLength;
/**
* Integer offset of the first byte of data in this chunk (the byte immediately
* following the chunk length 32-bits and the chunk type 32-bits) from byte zero
* of the source.
*/
public int dataPosition;
public int checksum;
public PngChunkMap(PngChunkCode code, int dataPosition, int dataLength, int checksum) {
this.code = code;
this.dataLength = dataLength;
this.dataPosition = dataPosition;
this.checksum = checksum;
}
@Override
public String toString() {
return "PngChunkMap{" +
"letters=" + code +
", dataLength=" + dataLength +
", dataPosition=" + dataPosition +
", checksum=" + checksum +
'}';
}
}
@@ -1,17 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.map;
import java.util.List;
/**
* A PngMap represents a map over an entire single PNG file.
* <p>
* WARNING: not sure if this API will remain.
* </p>
*/
public class PngMap {
public String source;
public List<PngChunkMap> chunks;
}
@@ -1,50 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.map;
import org.jackhuang.hmcl.ui.image.apng.PngChunkCode;
import org.jackhuang.hmcl.ui.image.apng.PngConstants;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.reader.PngReader;
import org.jackhuang.hmcl.ui.image.apng.reader.PngSource;
import java.io.IOException;
import java.util.ArrayList;
/**
* Simple processor that skips all chunk content and ignores checksums, with
* sole objective of building a map of the contents of a PNG file.
* <p>
* WARNING: not sure if this API will remain.
* </p>
*/
public class PngMapReader implements PngReader<PngMap> {
PngMap map;
public PngMapReader(String sourceName) {
map = new PngMap();
map.source = sourceName;
map.chunks = new ArrayList<>(4);
}
@Override
public boolean readChunk(PngSource source, int code, int dataLength) throws PngException, IOException {
int dataPosition = source.tell();
source.skip(dataLength);
int chunkChecksum = source.readInt();
map.chunks.add(new PngChunkMap(PngChunkCode.from(code), dataPosition, dataLength, chunkChecksum));
return code == PngConstants.IEND_VALUE;
}
@Override
public void finishedChunks(PngSource source) throws PngException, IOException {
// NOP
}
@Override
public PngMap getResult() {
return map;
}
}
@@ -1,4 +0,0 @@
/**
* @see <a href="https://github.com/aellerton/japng">japng</a>
*/
package org.jackhuang.hmcl.ui.image.apng;
@@ -1,65 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import org.jackhuang.hmcl.ui.image.apng.util.PartialInflaterInputStream;
import java.io.FilterInputStream;
import java.io.InputStream;
import java.util.zip.Inflater;
/**
* A BasicScanlineProcessor manages a re-entrant java.util.zip.Inflater object and
* basic bytesPerLine management.
*/
public abstract class BasicScanlineProcessor implements PngScanlineProcessor {
protected final int bytesPerLine;
protected Inflater inflater = new Inflater();
//protected PngHeader header;
//protected PngScanlineBuffer scanlineReader;
//protected PngFrameControl currentFrame;
//public DefaultScanlineProcessor(PngHeader header, PngScanlineBuffer scanlineReader, PngFrameControl currentFrame) {
public BasicScanlineProcessor(int bytesPerScanline) {
// this.header = header.adjustFor(currentFrame);
// this.currentFrame = currentFrame;
// this.scanlineReader = scanlineReader;
// this.bytesPerLine = this.header.bytesPerRow;
this.bytesPerLine = bytesPerScanline;
}
//@Override
//abstract public void processScanline(byte[] bytes, int position);
@Override
public FilterInputStream makeInflaterInputStream(InputStream inputStream) {
return new PartialInflaterInputStream(inputStream, inflater);
}
@Override
public int getBytesPerLine() {
return bytesPerLine;
}
@Override
public boolean isFinished() {
return inflater.finished();
}
// @Override
// public InflaterInputStream connect(InputStream inputStream) {
// multipartStream.add(inputStream);
// return iis;
// }
@Override
public void processTransparentGreyscale(byte k1, byte k0) {
// NOP by default
}
@Override
public void processTransparentTruecolour(byte r1, byte r0, byte g1, byte g0, byte b1, byte b0) {
// NOP by default
}
}
@@ -1,303 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import org.jackhuang.hmcl.ui.image.apng.PngAnimationType;
import org.jackhuang.hmcl.ui.image.apng.PngChunkCode;
import org.jackhuang.hmcl.ui.image.apng.PngConstants;
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.chunks.PngGamma;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import org.jackhuang.hmcl.ui.image.apng.map.PngChunkMap;
import java.io.IOException;
/**
* The DefaultPngChunkReader is the default PNG chunk-reading workhorse.
* <p>
* Note that any chunk types not recognised can be processed in the readOtherChunk()
* method.
*/
public class DefaultPngChunkReader<ResultT> implements PngChunkReader<ResultT> {
protected PngChunkProcessor<ResultT> processor;
protected boolean seenHeader = false;
protected int idatCount = 0;
protected int apngSequenceExpect = 0;
protected PngAnimationType animationType = PngAnimationType.NOT_ANIMATED;
//private PngMainImageOp mainImageOp = PngMainImageOp.MAIN_IMAGE_KEEP;
public DefaultPngChunkReader(PngChunkProcessor<ResultT> processor) {
this.processor = processor;
}
@Override
public boolean readChunk(PngSource source, int code, int dataLength) throws PngException, IOException {
int dataPosition = source.tell(); // note the start position before any further reads are done.
if (dataLength < 0) {
throw new PngIntegrityException(String.format("Corrupted read (Data length %d)", dataLength));
}
switch (code) {
case PngConstants.IHDR_VALUE:
readHeaderChunk(source, dataLength);
break;
case PngConstants.IEND_VALUE:
// NOP
break;
case PngConstants.gAMA_VALUE:
readGammaChunk(source, dataLength);
break;
case PngConstants.bKGD_VALUE:
readBackgroundChunk(source, dataLength);
break;
case PngConstants.tRNS_VALUE:
readTransparencyChunk(source, dataLength);
break;
case PngConstants.PLTE_VALUE:
readPaletteChunk(source, dataLength);
break;
case PngConstants.IDAT_VALUE:
readImageDataChunk(source, dataLength);
break;
case PngConstants.acTL_VALUE:
readAnimationControlChunk(source, dataLength);
break;
case PngConstants.fcTL_VALUE:
readFrameControlChunk(source, dataLength);
break;
case PngConstants.fdAT_VALUE:
readFrameImageDataChunk(source, dataLength);
break;
default:
readOtherChunk(code, source, dataPosition, dataLength);
break;
}
int chunkChecksum = source.readInt();
processChunkEnd(code, dataPosition, dataLength, chunkChecksum);
return code == PngConstants.IEND_VALUE;
}
@Override
public void processChunkEnd(int code, int dataPosition, int dataLength, int chunkChecksum) throws PngException {
processor.processChunkMapItem(new PngChunkMap(PngChunkCode.from(code), dataPosition, dataLength, chunkChecksum));
//container.chunks.add(new PngChunkMap(PngChunkCode.from(code), dataLength, dataPosition, chunkChecksum));
}
@Override
public void readHeaderChunk(PngSource source, int dataLength) throws IOException, PngException {
PngHeader header = PngHeader.from(source.getDis());
seenHeader = true;
processor.processHeader(header); // Hmm. Prefer header = PngHeader.from(source) ?
}
@Override
public void readGammaChunk(PngSource source, int dataLength) throws PngException, IOException {
processor.processGamma(PngGamma.from(source.getDis()));
}
@Override
public void readTransparencyChunk(PngSource source, int dataLength) throws IOException, PngException {
// TODO
//processor.processTransparency(PngTransparency.from(source, dataLength));
//throw new PngFeatureException("TODO UP TO HERE");
//source.skip(dataLength);
// if (dataLength % 3 != 0) {
// throw new PngIntegrityException(String.format("png spec: palette chunk length must be divisible by 3: %d", dataLength));
// }
if (source.supportsByteAccess()) {
processor.processTransparency(source.getBytes(), source.tell(), dataLength);
source.skip(dataLength);
} else {
byte[] paletteBytes = new byte[dataLength];
//ByteStreams.readFully(source.getBis(), paletteBytes);
source.getDis().readFully(paletteBytes);
processor.processTransparency(paletteBytes, 0, dataLength);
}
}
@Override
public void readBackgroundChunk(PngSource source, int dataLength) throws IOException, PngException {
if (!seenHeader) {
throw new PngIntegrityException("bKGD chunk received before IHDR chunk");
}
// TODO
//processor.processBackground(PngBackground.from(source, dataLength);
source.skip(dataLength);
}
@Override
public void readPaletteChunk(PngSource source, int dataLength) throws IOException, PngException {
if (dataLength % 3 != 0) {
throw new PngIntegrityException(String.format("png spec: palette chunk length must be divisible by 3: %d", dataLength));
}
// TODO: can check if colour type matches palette type, or if any palette received before (overkill?)
if (source.supportsByteAccess()) {
processor.processPalette(source.getBytes(), source.tell(), dataLength);
source.skip(dataLength);
} else {
byte[] paletteBytes = new byte[dataLength];
//ByteStreams.readFully(source.getBis(), paletteBytes);
source.getDis().readFully(paletteBytes);
processor.processPalette(paletteBytes, 0, dataLength);
}
}
@Override
public void readImageDataChunk(PngSource source, int dataLength) throws PngException, IOException {
if (idatCount == 0 && apngSequenceExpect == 0) {
// Processing a plain PNG (non animated) IDAT chunk
animationType = PngAnimationType.NOT_ANIMATED; // processor.chooseApngImageType(PngAnimationType.NOT_ANIMATED, null);
}
idatCount++;
switch (animationType) {
case ANIMATED_DISCARD_DEFAULT_IMAGE:
// do nothing
source.skip(dataLength);
break;
case ANIMATED_KEEP_DEFAULT_IMAGE:
processor.processFrameImageData(source.slice(dataLength), PngChunkCode.IDAT, source.tell(), dataLength);
break;
case NOT_ANIMATED:
default:
processor.processDefaultImageData(source.slice(dataLength), PngChunkCode.IDAT, source.tell(), dataLength);
break;
}
// source.skip(dataLength);
}
@Override
public void readAnimationControlChunk(PngSource source, int dataLength) throws IOException, PngException {
if (dataLength != PngConstants.LENGTH_acTL_CHUNK) {
throw new PngIntegrityException(String.format("acTL chunk length must be %d, not %d", PngConstants.LENGTH_acTL_CHUNK, dataLength));
}
processor.processAnimationControl(new PngAnimationControl(source.readInt(), source.readInt()));
}
@Override
public void readFrameControlChunk(PngSource source, int dataLength) throws IOException, PngException {
if (dataLength != PngConstants.LENGTH_fcTL_CHUNK) {
throw new PngIntegrityException(String.format("fcTL chunk length must be %d, not %d", PngConstants.LENGTH_fcTL_CHUNK, dataLength));
}
int sequence = source.readInt(); // TODO: check sequence # is correct or PngIntegrityException
if (sequence != apngSequenceExpect) {
throw new PngIntegrityException(String.format("fctl chunk expected sequence %d but received %d", apngSequenceExpect, sequence));
}
apngSequenceExpect++; // ready for next time
PngFrameControl frame = new PngFrameControl(
sequence,
source.readInt(), // width
source.readInt(), // height
source.readInt(), // x offset
source.readInt(), // y offset
source.readUnsignedShort(), // delay numerator
source.readUnsignedShort(), // delay denominator
source.readByte(), // dispose op
source.readByte() // blend op
);
if (sequence == 0) { // We're at the first frame...
if (idatCount == 0) { // Not seen any IDAT chunks yet
// APNG Spec says that when the first fcTL chunk is received *before* the first IDAT chunk
// the main image of the PNG becomes the first frame in the animation.
animationType = PngAnimationType.ANIMATED_KEEP_DEFAULT_IMAGE; //processor.chooseApngImageType(PngAnimationType.ANIMATED_KEEP_DEFAULT_IMAGE, frame);
//mainImageOp = PngMainImageOp.MAIN_IMAGE_STARTS_ANIMATION;
} else {
//mainImageOp = PngMainImageOp.MAIN_IMAGE_DISCARD;
animationType = PngAnimationType.ANIMATED_DISCARD_DEFAULT_IMAGE; // processor.chooseApngImageType(PngAnimationType.ANIMATED_DISCARD_DEFAULT_IMAGE, frame);
}
} else {
// fall through
}
processor.processFrameControl(frame);
}
//public abstract void setMainImageOp(PngMainImageOp op);
@Override
public void readFrameImageDataChunk(PngSource source, int dataLength) throws IOException, PngException {
// Note that once the sequence number is confirmed as being correct that there
// is no need to retain it in subsequent data.
int position = source.tell();
int sequence = source.readInt();
dataLength -= 4; // After reading the sequence number the data is just like IDAT.
if (sequence != apngSequenceExpect) {
throw new PngIntegrityException(String.format("fdAT chunk expected sequence %d but received %d", apngSequenceExpect, sequence));
}
apngSequenceExpect++; // for next time
//processFrameData(sequence, source, dataLength);
//processFrameImageData(source, dataLength);
processor.processFrameImageData(source.slice(dataLength), PngChunkCode.fdAT, source.tell(), dataLength);
// PngFrameControl current = container.getCurrentAnimationFrame();
//
// //imageDecoder
// // TODO: send image bytes to digester
// current.appendImageData(new PngChunkMap(PngChunkCode.fdAT, dataLength, position, 0));
//
// // TODO: skip everything except the frame sequence number
// source.skip(dataLength);
}
@Override
public void readOtherChunk(int code, PngSource source, int dataPosition, int dataLength) throws IOException {
// If we're not processing it, got to skip it.
source.skip(dataLength);
}
@Override
public void finishedChunks(PngSource source) throws PngException, IOException {
}
@Override
public ResultT getResult() {
return processor.getResult();
}
public boolean isSeenHeader() {
return seenHeader;
}
public int getIdatCount() {
return idatCount;
}
public int getApngSequenceExpect() {
return apngSequenceExpect;
}
public PngAnimationType getAnimationType() {
return animationType;
}
}
@@ -1,109 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* A simple way to process a PNG file is to read the entire thing into memory.
* <p>
* Very bad for very large images (500Mb?)? Yes.
* But simple for smaller (say <10Mb?) images? Also yes.
* I'd certainly like to make a lean alternative that processes bytes without reading
* the whole thing into memory, but it can come later. Patches welcome.
* <p>
* WARNING: I'm not sure I'll keep this. I may remove it and do everything with
* a BufferedInputStream + DataInputStream.
* </p>
*/
public class PngAtOnceSource implements PngSource {
final byte[] bytes;
//private String sourceDescription;
private final ByteArrayInputStream bis;
private final DataInputStream dis;
public PngAtOnceSource(byte[] bytes) { //}, String sourceDescription) {
this.bytes = bytes;
//this.sourceDescription = sourceDescription;
this.bis = new ByteArrayInputStream(this.bytes); // never closed because nothing to do for ByteArrayInputStream
this.dis = new DataInputStream(this.bis); // never closed because underlying stream doesn't need to be closed.
}
@Override
public int getLength() {
return bytes == null ? 0 : bytes.length;
}
@Override
public boolean supportsByteAccess() {
return true;
}
@Override
public byte[] getBytes() throws IOException {
return bytes;
}
@Override
public byte readByte() throws IOException {
return dis.readByte();
}
@Override
public short readUnsignedShort() throws IOException {
return (short) dis.readUnsignedShort();
}
@Override
public int readInt() throws IOException {
return dis.readInt();
}
@Override
public long skip(int chunkLength) throws IOException {
return dis.skip(chunkLength);
}
@Override
public int tell() {
return bytes.length - bis.available();
}
@Override
public int available() {
return bis.available();
}
// @Override
// public String getSourceDescription() {
// return sourceDescription;
// }
// @Override
// public ByteArrayInputStream getBis() {
// return bis;
// }
@Override
public DataInputStream getDis() {
return dis;
}
@Override
public InputStream slice(int dataLength) throws IOException {
// The below would be fine but in this case we have the full byte stream anyway...
//return ByteStreams.limit(bis, dataLength);
InputStream slice = new ByteArrayInputStream(bytes, tell(), dataLength);
this.skip(dataLength);
return slice;
}
public static PngAtOnceSource from(InputStream is) throws IOException {
return new PngAtOnceSource(is.readAllBytes());
}
}
@@ -1,137 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import org.jackhuang.hmcl.ui.image.apng.PngChunkCode;
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.chunks.PngGamma;
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.map.PngChunkMap;
import java.io.IOException;
import java.io.InputStream;
/**
* While a PngChunkReader does the low-level reading of a PNG file, it delegates <em>processing</em> of
* the data to a PngChunkProcessor instance. The class is generic about a specific result type because
* the intention of a chunk processor is to yield some result, which might be a bitmap, a sequence of
* bitmaps, a map of chunks, etc.
*/
public interface PngChunkProcessor<ResultT> {
/**
* Subclasses can further process the header immediately after PngHeader has been read.
* A subclass might elect to bail out of subsequent processing, e.g. if the image is too big
* or has an invalid format.
*
* @param header of image
*/
void processHeader(PngHeader header) throws PngException;
/**
* Process the PngGamma object parsed from a ``gaMA`` chunk.
*
* @param gamma parsed gamma data.
* @throws PngException
*/
void processGamma(PngGamma gamma) throws PngException;
/**
* Process the raw trNS data from the PNG file.
* <p>
* Note that this data is passed raw (but in a complete byte array).
* It is not parsed into an object because it allows concrete implementations to define
* their own way to handle the data. And it is not provided as in InputStream because
* the length of the data is well defined based on the PNG colour type.
*
* @param bytes array to read data from.
* @param position offset into the array to begin reading data from.
* @param length number of bytes to read from the array.
* @throws PngException
*/
void processTransparency(byte[] bytes, int position, int length) throws PngException;
/**
* A PLTE chunk has been read and needs to be processed.
* <p>
* A byte array is provided to process. A pre-defined palette class is not loaded because
* different rendering targets may elect to load the palette in different ways.
* For example, rendering to an ARGB palette may load the RGB data and organise as
* an array of 32-bit integer ARGB values.
* </p>
*
* @param bytes representing the loaded palette data. Each colour is represented by three
* bytes, 1 each for red, green and blue.
* @param position that the colour values begin in bytes
* @param length number of bytes that the palette bytes continue. Guaranteed to be a
* multiple of 3.
* @throws PngException
*/
void processPalette(byte[] bytes, int position, int length) throws PngException;
/**
* Process one IDAT chunk the "default image" bitmap in a given file.
* <p>
* The "default", "main" or "primary image" of a file is what you'd generally
* think of as "the" image in file, namely the image constructed of IDAT chunks
* and displayed by any normal viewer or browser.
* <p>
* I theorise (without proof) that most PNG files in the wild have a single
* IDAT chunk representing all pixels in the main image, but the PNG specification
* requires loaders to handle the case where there are multiple IDAT chunks.
* In the case of multiple chunks the data continues precisely where the last
* IDAT chunk left off. The same applies to fdAT chunks.
* <p>
* The data is provided not in any pre-parsed object or even as a byte array because
* the bytes can be arbitrarily long. The InputStream provided is a slice of the
* containing source and will terminate at the end of the IDAT chunk.
*
* @param inputStream data source containing all bytes in the image data chunk.
* @param code of the chunk which should always be IDAT in this case.
* Compare to this.processFrameImageData.
* @param position absolute position within the file. hmmm may be removed
* @param length length of bytes of the hunk. hmmm may be removed
* @throws IOException
* @throws PngException
*/
void processDefaultImageData(InputStream inputStream, PngChunkCode code, int position, int length) throws IOException, PngException;
void processAnimationControl(PngAnimationControl animationControl) throws PngException;
void processFrameControl(PngFrameControl frameControl) throws PngException;
/**
* The reader has determined that bitmap data needs to be processed for an animation frame.
* The image data may be from an IDAT chunk or an fdAT chunk.
* <p>
* Whether the "default image" in a PNG file is to be part of the animation or discarded
* is determed by the placement of the first fcTL chunk in the file. See the APNG specification.
*
* @param inputStream data source containing all bytes in the frame image data chunk.
* @param code either IDAT or fdAT. Designed to allow an implementation to detect whether the
* frame is from the "main image" or a subsequent image. May not be necessary.
* @param position absolute position within the file. hmmm may be removed
* @param length length of bytes of the hunk. hmmm may be removed
* @throws IOException
* @throws PngException
*/
void processFrameImageData(InputStream inputStream, PngChunkCode code, int position, int length) throws IOException, PngException;
/**
* A subclass can record data about the chunk here.
* <p>
* WARNING: this API may be removed. I'm not sure if it is useful.
*
* @param chunkMapItem represents a "map" of a single chunk in the file.
* @throws PngException
*/
void processChunkMapItem(PngChunkMap chunkMapItem) throws PngException;
/**
* @return the result of processing all chunks. Only ever called after the file is fully read.
*/
ResultT getResult();
}
@@ -1,134 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import java.io.IOException;
/**
* Created by aellerton on 15/05/2015.
*/
public interface PngChunkReader<ResultT> extends PngReader<ResultT> {
@Override
boolean readChunk(PngSource source, int code, int dataLength) throws PngException, IOException;
void processChunkEnd(int code, int dataPosition, int dataLength, int chunkChecksum) throws PngException;
void readHeaderChunk(PngSource source, int dataLength) throws IOException, PngException;
void readGammaChunk(PngSource source, int dataLength) throws PngException, IOException;
/**
* Process the tRNS chunk.
* <p>
* From http://www.w3.org/TR/PNG/#11tRNS:
* <p>
* The tRNS chunk specifies either alpha values that are associated with palette entries (for indexed-colour
* images) or a single transparent colour (for greyscale and truecolour images). The tRNS chunk contains:
* <p>
* Colour type 0:
* <p>
* Grey sample value 2 bytes
* <p>
* Colour type 2:
* <p>
* Red sample value 2 bytes
* Blue sample value 2 bytes
* Green sample value 2 bytes
* <p>
* Colour type 3:
* <p>
* Alpha for palette index 0 1 byte
* Alpha for palette index 1 1 byte
* ...etc... 1 byte
* <p>
* Note that for palette colour types the number of transparency entries may be less than the number of
* entries in the palette. In that case all missing entries are assumed to be fully opaque.
*
* @param source
* @param dataLength
*/
void readTransparencyChunk(PngSource source, int dataLength) throws IOException, PngException;
/**
* Process the bKGD chunk.
* <p>
* From http://www.w3.org/TR/PNG/#11bKGD:
* <p>
* The bKGD chunk specifies a default background colour to present the image against.
* If there is any other preferred background, either user-specified or part of a larger
* page (as in a browser), the bKGD chunk should be ignored. The bKGD chunk contains:
* <p>
* Colour types 0 and 4
* Greyscale 2 bytes
* Colour types 2 and 6
* Red 2 bytes
* Green 2 bytes
* Blue 2 bytes
* Colour type 3
* Palette index 1 byte
* <p>
* For colour type 3 (indexed-colour), the value is the palette index of the colour to be used as background.
* <p>
* For colour types 0 and 4 (greyscale, greyscale with alpha), the value is the grey level to be used as
* background in the range 0 to (2bitdepth)-1. For colour types 2 and 6 (truecolour, truecolour with alpha),
* the values are the colour to be used as background, given as RGB samples in the range 0 to (2bitdepth)-1.
* In each case, for consistency, two bytes per sample are used regardless of the image bit depth. If the image
* bit depth is less than 16, the least significant bits are used and the others are 0.
*
* @param source
* @param dataLength
* @throws IOException
*/
void readBackgroundChunk(PngSource source, int dataLength) throws IOException, PngException;
void readPaletteChunk(PngSource source, int dataLength) throws IOException, PngException;
/**
* Read the IDAT chunk.
* <p>
* The default implementation skips the data, deferring to the finishedChunks() method
* to process the data. Key reasons to do this:
* <ul>
* <li>There could be multiple IDAT chunks and the streams need to
* be concatenated. This could be done in a single pass but it's more
* complicated and is not the objective of this implementation.</li>
* <li>This might be an APNG file and the IDAT chunk(s) are to be skipped.</li>
* </ul>
*
* @param source to read from
* @param dataLength in bytes of the data
* @throws PngException
* @throws IOException
*/
void readImageDataChunk(PngSource source, int dataLength) throws PngException, IOException;
void readAnimationControlChunk(PngSource source, int dataLength) throws IOException, PngException;
/**
* Read the fcTL chunk.
* <p>
* See https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
*
* @param source
* @param dataLength
* @throws IOException
* @throws PngException
*/
void readFrameControlChunk(PngSource source, int dataLength) throws IOException, PngException;
void readFrameImageDataChunk(PngSource source, int dataLength) throws IOException, PngException;
/**
* Give subclasses the opportunity to process a chunk code that was not recognised.
*
* @param code chunk type as integer.
* @param source of PNG data, positioned to read the data bytes.
* @param dataPosition offset from absolute start of bytes that data beings.
* @param dataLength of this chunk
* @throws IOException
*/
void readOtherChunk(int code, PngSource source, int dataPosition, int dataLength) throws IOException;
}
@@ -1,112 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import org.jackhuang.hmcl.ui.image.apng.PngColourType;
import org.jackhuang.hmcl.ui.image.apng.PngConstants;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* PNG reading support functions.
*/
public class PngReadHelper {
/**
* Return true if the next 8 bytes of the InputStream match the
* standard 8 byte PNG Signature, and false if they do not match.
* <p>
* If the stream ends before the signature is read an EOFExceptoin is thrown.
* </p><p>
* Note that no temporary buffer is allocated.
* </p>
*
* @param is Stream to read the 8-byte PNG signature from
* @throws IOException in the case of any IO exception, including EOF.
*/
public static boolean readSignature(InputStream is) throws IOException {
for (int i = 0; i < PngConstants.LENGTH_SIGNATURE; i++) {
int b = is.read();
if (b < 0) {
throw new EOFException();
}
if ((byte) b != PngConstants.BYTES_SIGNATURE[i]) {
return false;
}
}
return true;
}
/**
* Reads a given InputStream using the PngReader to process all chunks until the file is
* finished, then returning the result from the PngReader.
*
* @param is stream to read
* @param reader reads and delegates processing of all chunks
* @param <ResultT> result of the processing
* @return result of the processing of the InputStream.
* @throws PngException
*/
public static <ResultT> ResultT read(InputStream is, PngReader<ResultT> reader) throws PngException {
try {
if (!PngReadHelper.readSignature(is)) {
throw new PngException(PngConstants.ERROR_NOT_PNG, "Failed to read PNG signature");
}
// PngAtOnceSource source = PngAtOnceSource.from(is);//, sourceName);
PngSource source = new PngStreamSource(is);
boolean finished = false;
while (!finished) {
int length = source.readInt();
int code = source.readInt();
finished = reader.readChunk(source, code, length);
}
if (source.available() > 0) { // Should trailing data after IEND always be error or can configure as warning?
throw new PngException(PngConstants.ERROR_EOF_EXPECTED, String.format("Completed IEND but %d byte(s) remain", source.available()));
}
reader.finishedChunks(source);
return reader.getResult();
} catch (EOFException e) {
throw new PngException(PngConstants.ERROR_EOF, "Unexpected EOF", e);
} catch (IOException e) {
throw new PngException(PngConstants.ERROR_UNKNOWN_IO_FAILURE, e.getMessage(), e);
}
}
/**
* Number of bytes per row is key to processing scanlines.
* <p>
* TODO: should this by on the header?
*/
public static int calculateBytesPerRow(int pixelsPerRow, byte bitDepth, PngColourType colourType, byte interlaceMethod) {
if (interlaceMethod != 0) {
throw new IllegalStateException("Interlaced images not yet supported");
} else {
int numComponentsPerPixel = colourType.componentsPerPixel; // or "channels". e.g. "gray and alpha" means two components.
int bitsPerComponent = bitDepth; // e.g. "4" means 4 bits for gray, 4 bits for alpha
int bitsPerPixel = bitsPerComponent * numComponentsPerPixel; // e.g. total of 8 bits per pixel
int bitsPerRow = bitsPerPixel * pixelsPerRow;
//?? use that (bitDepth+7)>>3 ... thing?
// unsigned int bpp = (row_info->pixel_depth + 7) >> 3; // libpng
// If there are less than 8 bits per pixel, then ensure the last byte of the row is padded.
int bytesPerRow = bitsPerRow / 8 + ((0 == (bitsPerRow % 8)) ? 0 : (8 - bitsPerRow % 8));
return 1 + bytesPerRow; // need 1 byte for filter code
}
}
private PngReadHelper() {
}
}
@@ -1,20 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import java.io.IOException;
/**
* All PngReader implementations need to read a specific single chunk and to return
* a result of some form.
*/
public interface PngReader<ResultT> {
boolean readChunk(PngSource source, int code, int dataLength) throws PngException, IOException;
void finishedChunks(PngSource source) throws PngException, IOException;
ResultT getResult();
}
@@ -1,69 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import java.io.FilterInputStream;
import java.io.InputStream;
/**
* A PngScanlineProcessor receives decompressed, de-filtered scanlines, one by one, in order
* from top to bottom. Interlaced scanlines are not yet supported. The scanline data is not
* parsed or expanded in any form, so if the PNG is 1-bit monochrome or 16 bit RGBA, the bytes
* are provided as-is. The job of the processor is to read and reformat each pixel as
* appropriate for the destination bitmap.
* <p>
* Note that a single instance of a PngScanlineProcessor is intended to process a single
* bitmap and that, when the bitmap is complete, the isFinished() method will return true.
* In the case of an animated PNG, it is expected that a new PngScanlineProcessor will be
* created to service the new bitmap.
* <p>
* Note: I wonder if this would be better named PngScanlineTransformer because its primary
* purpose is to convert pixels from raw file format to destination format.
*/
public interface PngScanlineProcessor {
/**
* A PngScanlineProcessor is responsible for decompressing the raw (compressed) data.
* This is important because there can be more than one IDAT and fdAT chunks for a single
* image, and decompression must occur seamlessly across those chunks, so the decompression
* must be stateful across multiple invocations of makeInflaterInputStream. In practice, it
* seems to be rare that there are multiple image data chunks for a single image.
*
* @param inputStream stream over raw compressed PNG image data.
* @return an InflaterInputStream that decompresses the current stream.
*/
FilterInputStream makeInflaterInputStream(InputStream inputStream);
/**
* The processScanline method is invoked when the raw image data has been first decompressed
* then de-filtered. The PngScanlineProcessor then must interpret each byte according to the
* specific image format and render it to the destination as appropriate.
*
* @param bytes decompressed, de-filtered bytes, ready for processing.
* @param position the position that scanline pixels begin in the array. Note that it is
* the responsibility of the PngScanlineProcessor to know exactly how many
* bytes are in the row. This is because a single processor might process
* different frames in an APNG, and each frame can have a different width
* (up to the maximum set in the PNG header).
*/
void processScanline(byte[] bytes, int position);
int getBytesPerLine();
void processTransparentGreyscale(byte k1, byte k0);
void processTransparentTruecolour(byte r1, byte r0, byte g1, byte g0, byte b1, byte b0);
/**
* A PngScanlineProcessor must be able to decompress more than one consecutive IDAT or fdAT
* chunks. This is because the PNG specification states that a valid PNG file can have more
* than one consecutive IDAT (and by extension, fdAT) chunks and the data therein must be
* treated as if concatenated.
*
* @return true when the data for the current bitmap is complete.
*/
boolean isFinished();
//InflaterInputStream connect(InputStream inputStream);
}
@@ -1,43 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* The API for reading any PNG source.
*
* @see PngAtOnceSource
* <p>
* WARNING: this may be removed in favour of direct use of InputStream objects.
*/
public interface PngSource {
int getLength();
boolean supportsByteAccess();
byte[] getBytes() throws IOException;
byte readByte() throws IOException;
short readUnsignedShort() throws IOException;
int readInt() throws IOException;
long skip(int chunkLength) throws IOException;
int tell();
int available();
// String getSourceDescription();
// ByteArrayInputStream getBis();
DataInputStream getDis();
InputStream slice(int dataLength) throws IOException;
}
@@ -1,91 +0,0 @@
// Copy from https://github.com/aellerton/japng
// Licensed under the Apache License, Version 2.0.
package org.jackhuang.hmcl.ui.image.apng.reader;
import org.jackhuang.hmcl.ui.image.apng.util.InputStreamSlice;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Created by aellerton on 13/06/2015.
*/
public class PngStreamSource implements PngSource {
final InputStream src;
//private final ByteArrayInputStream bis;
private final DataInputStream dis;
public PngStreamSource(InputStream src) {
this.src = src;
//this.bis = new ByteArrayInputStream(this.bytes); // never closed because nothing to do for ByteArrayInputStream
this.dis = new DataInputStream(this.src); // never closed because underlying stream doesn't need to be closed.
}
@Override
public int getLength() {
return 0; // TODO?
}
@Override
public boolean supportsByteAccess() {
return false;
}
@Override
public byte[] getBytes() throws IOException {
return null;
}
@Override
public byte readByte() throws IOException {
return dis.readByte();
}
@Override
public short readUnsignedShort() throws IOException {
return (short) dis.readUnsignedShort();
}
@Override
public int readInt() throws IOException {
return dis.readInt();
}
@Override
public long skip(int chunkLength) throws IOException {
return dis.skip(chunkLength);
}
@Override
public int tell() {
return 0; // TODO
}
@Override
public int available() {
try {
return dis.available(); // TODO: adequate?
} catch (IOException e) {
return 0; // TODO
}
}
// @Override
// public ByteArrayInputStream getBis() {
// return null;
// }
@Override
public DataInputStream getDis() {
return dis;
}
@Override
public InputStream slice(int dataLength) {
return new InputStreamSlice(src, dataLength);
}
}
Loaded 100 of 320 files, more files were not shown because too many files have changed in this diff. Show more