Skip to content

Commit

Permalink
Add support storing images locally on the server
Browse files Browse the repository at this point in the history
  • Loading branch information
zbx1425 committed Jan 25, 2025
1 parent ef2daf9 commit b46412a
Show file tree
Hide file tree
Showing 18 changed files with 535 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import cn.zbx1425.worldcomment.data.network.upload.ImageUploader;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.minecraft.client.DeltaTracker;

Check failure on line 7 in common/src/main/java/cn/zbx1425/worldcomment/ClientConfig.java

View workflow job for this annotation

GitHub Actions / build (1.20.4)

[Task :common:compileJava FAILED] cannot find symbol
import net.minecraft.network.FriendlyByteBuf;
Expand All @@ -24,7 +25,7 @@ public class ClientConfig {

public static ClientConfig fromServerConfig(ServerConfig serverConfig) {
ClientConfig config = new ClientConfig();
config.imageUploader = serverConfig.parseUploaderList();
config.imageUploader = ImageUploader.parseUploaderList(serverConfig.parseUploaderConfig());
config.allowMarkerUsage = switch (serverConfig.allowMarkerUsage.value) {
case "op" -> 0;
case "creative" -> 1;
Expand All @@ -49,10 +50,11 @@ public ClientConfig() {

public void readPacket(FriendlyByteBuf packet) {
int uploaderCount = packet.readInt();
imageUploader = new ArrayList<>();
List<JsonObject> uploaderConfigs = new ArrayList<>();
for (int i = 0; i < uploaderCount; i++) {
imageUploader.add(ImageUploader.getUploader(JsonParser.parseString(packet.readUtf()).getAsJsonObject()));
uploaderConfigs.add(JsonParser.parseString(packet.readUtf()).getAsJsonObject());
}
imageUploader = ImageUploader.parseUploaderList(uploaderConfigs);
allowMarkerUsage = packet.readInt();
}

Expand Down
8 changes: 8 additions & 0 deletions common/src/main/java/cn/zbx1425/worldcomment/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public static void init(RegistriesWrapper registries) {
ServerPlatform.registerPacket(PacketEntryUpdateS2C.IDENTIFIER);
ServerPlatform.registerPacket(PacketRegionDataS2C.IDENTIFIER);
ServerPlatform.registerPacket(PacketRegionRequestC2S.IDENTIFIER);
ServerPlatform.registerPacket(PacketImageUploadC2S.IDENTIFIER);
ServerPlatform.registerPacket(PacketImageUploadS2C.IDENTIFIER);
ServerPlatform.registerPacket(PacketImageDownloadC2S.IDENTIFIER);
ServerPlatform.registerPacket(PacketImageDownloadS2C.IDENTIFIER);

ServerPlatform.registerNetworkReceiver(
PacketRegionRequestC2S.IDENTIFIER, PacketRegionRequestC2S::handle);
Expand All @@ -61,6 +65,10 @@ public static void init(RegistriesWrapper registries) {
PacketEntryCreateC2S.IDENTIFIER, PacketEntryCreateC2S::handle);
ServerPlatform.registerNetworkReceiver(
PacketEntryActionC2S.IDENTIFIER, PacketEntryActionC2S::handle);
ServerPlatform.registerNetworkReceiver(
PacketImageUploadC2S.IDENTIFIER, PacketImageUploadC2S::handle);
ServerPlatform.registerNetworkReceiver(
PacketImageDownloadC2S.IDENTIFIER, PacketImageDownloadC2S::handle);

ServerPlatform.registerServerStartingEvent(server -> {
try {
Expand Down
9 changes: 5 additions & 4 deletions common/src/main/java/cn/zbx1425/worldcomment/MainClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

import cn.zbx1425.worldcomment.data.client.ClientWorldData;
import cn.zbx1425.worldcomment.interop.BulletChatInterop;
import cn.zbx1425.worldcomment.network.PacketClientConfigS2C;
import cn.zbx1425.worldcomment.network.PacketCollectionDataS2C;
import cn.zbx1425.worldcomment.network.PacketRegionDataS2C;
import cn.zbx1425.worldcomment.network.PacketEntryUpdateS2C;
import cn.zbx1425.worldcomment.network.*;
import com.mojang.blaze3d.platform.InputConstants;
import net.minecraft.client.KeyMapping;
import org.lwjgl.glfw.GLFW;
Expand All @@ -27,6 +24,10 @@ public static void init() {
PacketEntryUpdateS2C.IDENTIFIER, PacketEntryUpdateS2C.ClientLogics::handle);
ClientPlatform.registerNetworkReceiver(
PacketClientConfigS2C.IDENTIFIER, PacketClientConfigS2C.ClientLogics::handle);
ClientPlatform.registerNetworkReceiver(
PacketImageUploadS2C.IDENTIFIER, PacketImageUploadS2C.ClientLogics::handle);
ClientPlatform.registerNetworkReceiver(
PacketImageDownloadS2C.IDENTIFIER, PacketImageDownloadS2C.ClientLogics::handle);

ClientPlatform.registerPlayerJoinEvent(ignored -> {
ClientWorldData.INSTANCE.clear();
Expand Down
11 changes: 5 additions & 6 deletions common/src/main/java/cn/zbx1425/worldcomment/ServerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,21 @@ public void save(Path configPath) throws IOException {
Files.writeString(configPath, new GsonBuilder().setPrettyPrinting().create().toJson(json));
}

public List<ImageUploader> parseUploaderList() {
List<ImageUploader> uploaderList = new ArrayList<>();
public List<JsonObject> parseUploaderConfig() {
List<JsonObject> uploaderConfigs = new ArrayList<>();
try {
JsonElement rootElement = JsonParser.parseString(imageUploadConfig.value);
if (rootElement.isJsonArray()) {
for (JsonElement element : rootElement.getAsJsonArray()) {
uploaderList.add(ImageUploader.getUploader(element.getAsJsonObject()));
uploaderConfigs.add(element.getAsJsonObject());
}
} else if (rootElement.isJsonObject()) {
uploaderList.add(ImageUploader.getUploader(rootElement.getAsJsonObject()));
uploaderConfigs.add(rootElement.getAsJsonObject());
}
} catch (Exception ex) {
Main.LOGGER.error("Failed to parse image upload config", ex);
}
uploaderList.add(ImageUploader.NoopUploader.INSTANCE);
return uploaderList;
return uploaderConfigs;
}

public static class ConfigItem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static void executeCommandServer(CommentEntry comment, ServerWorldData wo
case "domainisnot" -> predicateIsPositive = false;
default -> { return; }
};
ImageUploader uploader = Main.SERVER_CONFIG.parseUploaderList().getFirst();
ImageUploader uploader = ImageUploader.parseUploaderList(Main.SERVER_CONFIG.parseUploaderConfig()).getFirst();
for (CommentEntry commentEntry : worldData.comments.timeIndex.values()) {
if (commentEntry.image.url.isEmpty()) continue;
URI imageUrl;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;

public class ImageConvert {
public class ImageConvertClient {

public static byte[] toJpeg(byte[] pngImageBytes) {
ByteBuffer offHeapPngData = OffHeapAllocator.allocate(pngImageBytes.length);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cn.zbx1425.worldcomment.data.network;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;

public class ImageConvertServer {

public static byte[] toJpegScaled(byte[] pngImageBytes, int maxWidth) throws IOException {
BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(pngImageBytes));
if (originalImage == null) {
throw new IOException("Failed to read image");
}

int originalWidth = originalImage.getWidth();
int originalHeight = originalImage.getHeight();
int thumbWidth = Math.min(originalWidth, maxWidth);
int thumbHeight = (int) ((float) originalHeight * thumbWidth / originalWidth);

BufferedImage thumbImage = new BufferedImage(thumbWidth, thumbHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = thumbImage.createGraphics();
try {
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

g2d.setColor(Color.BLACK);
g2d.fillRect(0, 0, thumbWidth, thumbHeight);

g2d.drawImage(originalImage, 0, 0, thumbWidth, thumbHeight, null);
} finally {
g2d.dispose();
}

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();

JPEGImageWriteParam writeParam = new JPEGImageWriteParam(Locale.getDefault());
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
writeParam.setCompressionQuality(0.90f);
writeParam.setOptimizeHuffmanTables(true);

try (MemoryCacheImageOutputStream output = new MemoryCacheImageOutputStream(outputStream)) {
writer.setOutput(output);
writer.write(null, new IIOImage(thumbImage, null, null), writeParam);
} finally {
writer.dispose();
}

return outputStream.toByteArray();
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package cn.zbx1425.worldcomment.data.network;

import cn.zbx1425.worldcomment.BuildConfig;
import cn.zbx1425.worldcomment.Main;
import cn.zbx1425.worldcomment.data.network.upload.ImageUploader;
import cn.zbx1425.worldcomment.data.network.upload.LocalStorageUploader;
import cn.zbx1425.worldcomment.util.OffHeapAllocator;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.AbstractTexture;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.resources.ResourceLocation;
import org.apache.commons.codec.digest.DigestUtils;
import org.lwjgl.system.MemoryUtil;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
Expand All @@ -28,9 +25,6 @@
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.stream.Stream;

public class ImageDownload {

Expand All @@ -48,6 +42,7 @@ public static AbstractTexture getTexture(ThumbImage image, boolean thumb) {
byte[] localImageData = getLocalImageData(image.url);
if (localImageData != null) {
applyImageData(targetUrl, localImageData);
return;
}
} catch (IOException ex) {
Main.LOGGER.warn("Cannot read local image {}", image.url, ex);
Expand All @@ -60,26 +55,39 @@ public static AbstractTexture getTexture(ThumbImage image, boolean thumb) {
}

private static void downloadImage(String url) {
HttpRequest request = ImageUploader.requestBuilder(URI.create(url))
.timeout(Duration.of(10, ChronoUnit.SECONDS))
.GET()
.build();
Main.HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofByteArray())
.thenAccept(response -> {
if (response.statusCode() != 200) {
throw new CompletionException(new IOException("HTTP Error Code " + response.statusCode()));
}
byte[] imageData = response.body();
applyImageData(url, imageData);
})
.exceptionally(ex -> {
Main.LOGGER.warn("Cannot download image {}", url, ex);
synchronized (images) {
if (!images.containsKey(url)) return null;
images.get(url).failed = true;
}
return null;
});
if (url.startsWith(LocalStorageUploader.URL_PREFIX)) {
LocalStorageUploader.downloadImage(url)
.thenAccept(imageData -> applyImageData(url, imageData))
.exceptionally(ex -> {
Main.LOGGER.warn("Cannot download image {}", url, ex);
synchronized (images) {
if (!images.containsKey(url)) return null;
images.get(url).failed = true;
}
return null;
});
} else {
HttpRequest request = ImageUploader.requestBuilder(URI.create(url))
.timeout(Duration.of(10, ChronoUnit.SECONDS))
.GET()
.build();
Main.HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofByteArray())
.thenAccept(response -> {
if (response.statusCode() != 200) {
throw new CompletionException(new IOException("HTTP Error Code " + response.statusCode()));
}
byte[] imageData = response.body();
applyImageData(url, imageData);
})
.exceptionally(ex -> {
Main.LOGGER.warn("Cannot download image {}", url, ex);
synchronized (images) {
if (!images.containsKey(url)) return null;
images.get(url).failed = true;
}
return null;
});
}
}

private static byte[] getLocalImageData(String url) throws IOException {
Expand All @@ -96,7 +104,7 @@ private static void applyImageData(String url, byte[] pngOrJpgImageData) {
byte[] imageData = pngOrJpgImageData;
if (url.toLowerCase(Locale.ROOT).endsWith(".jpg")) {
// Actually maybe directly construct NativeImage from jpg
imageData = ImageConvert.toPng(imageData);
imageData = ImageConvertClient.toPng(imageData);
}
ByteBuffer buffer = OffHeapAllocator.allocate(imageData.length);
buffer.put(imageData);
Expand Down Expand Up @@ -136,27 +144,26 @@ private static AbstractTexture queryTexture(String url) {
public static void purgeUnused() {
long currentTime = System.currentTimeMillis();
synchronized (images) {
for (Iterator<Map.Entry<String, ImageState>> it = images.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, ImageState> entry = it.next();
if (currentTime - entry.getValue().queryTime > 60000) {
if (entry.getValue().texture != null) entry.getValue().texture.close();
it.remove();
Iterator<Map.Entry<String, ImageState>> iterator = images.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, ImageState> entry = iterator.next();
if (currentTime - entry.getValue().lastQueryTime > 60000) {
if (entry.getValue().texture != null) {
entry.getValue().texture.close();
}
iterator.remove();
}
}
}
}

private static class ImageState {
public long queryTime;
public DynamicTexture texture;
public boolean failed;

public ImageState() {
queryTime = System.currentTimeMillis();
}
public long lastQueryTime;

public void onQuery() {
queryTime = System.currentTimeMillis();
lastQueryTime = System.currentTimeMillis();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import cn.zbx1425.worldcomment.data.CommentEntry;
import cn.zbx1425.worldcomment.data.ServerWorldData;
import cn.zbx1425.worldcomment.data.network.upload.ImageUploader;
import cn.zbx1425.worldcomment.data.network.upload.ImageUploadConfig;
import cn.zbx1425.worldcomment.network.PacketEntryCreateC2S;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
Expand All @@ -24,34 +23,33 @@ public class SubmitDispatcher {
private static final Long2ObjectMap<SubmitJob> pendingJobs = new Long2ObjectOpenHashMap<>();

public static long addJob(CommentEntry comment, byte[] imageBytes, BiConsumer<SubmitJob, Throwable> callback) {
long jobId = ServerWorldData.SNOWFLAKE.nextId();
SubmitJob job = new SubmitJob(comment, imageBytes, callback, MainClient.CLIENT_CONFIG);
addJob(jobId, job);
return jobId;
addJob(comment.id, job);
return comment.id;
}

private static void addJob(long jobId, SubmitJob job) {
synchronized (pendingJobs) {
pendingJobs.put(jobId, job);
}
if (job.imageBytes != null) {
ImageUploader uploader = job.uploaderToUse.poll();
if (uploader == null) throw new IllegalStateException("All uploads failed");
uploader.uploadImage(job.imageBytes, job.comment)
.thenAccept(thumbImage -> {
job.setImage(thumbImage);
trySendPackage(jobId);
})
.exceptionally(ex -> {
Main.LOGGER.error("Upload Image", ex);
if (job.callback != null) job.callback.accept(job, ex);
if (job.uploaderToUse.isEmpty()) {
removeJob(jobId);
} else {
addJob(jobId, job);
}
return null;
});
ImageUploader uploader = job.uploaderToUse.poll();
if (uploader == null) throw new IllegalStateException("All uploads failed");
uploader.uploadImage(job.imageBytes, job.comment)
.thenAccept(thumbImage -> {
job.setImage(thumbImage);
trySendPackage(jobId);
})
.exceptionally(ex -> {
Main.LOGGER.error("Upload Image", ex);
if (job.callback != null) job.callback.accept(job, ex);
if (job.uploaderToUse.isEmpty()) {
removeJob(jobId);
} else {
addJob(jobId, job);
}
return null;
});
}
}

Expand Down
Loading

0 comments on commit b46412a

Please sign in to comment.