Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make PNJ the default map image io backend #306

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ dependencies {
api(libs.htmlSanitizerJ10) {
isTransitive = false // depends on guava, provided by mc at runtime
}

implementation(libs.pnj)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import xyz.jpenilla.squaremap.common.data.DirectoryProvider;
import xyz.jpenilla.squaremap.common.data.image.BufferedImageMapImageIO;
import xyz.jpenilla.squaremap.common.data.image.MapImageIO;
import xyz.jpenilla.squaremap.common.data.image.PNJMapImageIO;

@SuppressWarnings("unused")
public final class Config extends AbstractConfig {
private static final int LATEST_VERSION = 2;
private static final int LATEST_VERSION = 3;

Config(final DirectoryProvider directoryProvider) {
super(directoryProvider.dataDirectory(), Config.class, "config.yml", LATEST_VERSION);
Expand All @@ -26,7 +29,26 @@ protected void addVersions(final ConfigurationTransformation.VersionedBuilder ve
})
.build();

versionedBuilder.addVersion(LATEST_VERSION, oneToTwo);
final ConfigurationTransformation twoToThree = ConfigurationTransformation.chain(
ConfigurationTransformation.builder()
.addAction(NodePath.path("settings", "image-quality"), (path, node) -> new Object[]{"settings", "image-io"})
.build(),
ConfigurationTransformation.builder()
.addAction(NodePath.path("settings", "image-io", "compress-images"), (path, node) -> new Object[]{"settings", "image-io", "bufferedimage", "compress-images"})
.build(),
ConfigurationTransformation.builder()
.addAction(NodePath.path("settings", "image-io"), (path, node) -> {
final boolean compress = node.node("bufferedimage", "compress-images", "enabled").getBoolean();
if (compress) {
node.node("backend").set("bufferedimage");
}
return null;
})
.build()
);

versionedBuilder.addVersion(2, oneToTwo);
versionedBuilder.addVersion(LATEST_VERSION, twoToThree);
}

static Config config;
Expand Down Expand Up @@ -59,11 +81,18 @@ private static void webDirSettings() {
public static boolean COMPRESS_IMAGES = false;
private static double COMPRESSION_RATIO_CONFIG = 0.0F;
public static float COMPRESSION_RATIO;
public static MapImageIO<?> MAP_IMAGE_IO;

private static void imageQualitySettings() {
COMPRESS_IMAGES = config.getBoolean("settings.image-quality.compress-images.enabled", COMPRESS_IMAGES);
COMPRESSION_RATIO_CONFIG = config.getDouble("settings.image-quality.compress-images.value", COMPRESSION_RATIO_CONFIG);
private static void imageIOSettings() {
COMPRESS_IMAGES = config.getBoolean("settings.image-io.bufferedimage.compress-images.enabled", COMPRESS_IMAGES);
COMPRESSION_RATIO_CONFIG = config.getDouble("settings.image-io.bufferedimage.compress-images.value", COMPRESSION_RATIO_CONFIG);
COMPRESSION_RATIO = (float) (1.0D - COMPRESSION_RATIO_CONFIG);
final String imageIoBackend = config.getString("settings.image-io.backend", "pnj");
MAP_IMAGE_IO = switch (imageIoBackend) {
case "bufferedimage" -> new BufferedImageMapImageIO();
case "pnj" -> new PNJMapImageIO();
default -> throw new IllegalArgumentException("Invaild image io backend: " + imageIoBackend + "; Supported options: [pnj, bufferedimage]");
};
}

public static boolean HTTPD_ENABLED = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import xyz.jpenilla.squaremap.common.config.ConfigManager;
import xyz.jpenilla.squaremap.common.config.WorldAdvanced;
import xyz.jpenilla.squaremap.common.config.WorldConfig;
import xyz.jpenilla.squaremap.common.data.image.MapImage;
import xyz.jpenilla.squaremap.common.layer.SpawnIconLayer;
import xyz.jpenilla.squaremap.common.layer.WorldBorderLayer;
import xyz.jpenilla.squaremap.common.task.render.RenderFactory;
Expand Down Expand Up @@ -146,7 +147,7 @@ public int getMapColor(final BlockState state) {
return Colors.rgb(state.getMapColor(null, null));
}

public void saveImage(final Image image) {
public void saveImage(final MapImage image) {
this.imageIOExecutor.saveImage(image);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package xyz.jpenilla.squaremap.common.data.image;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
import xyz.jpenilla.squaremap.common.config.Config;

@DefaultQualifier(NonNull.class)
public final class BufferedImageMapImageIO implements MapImageIO<BufferedImageMapImageIO.BufferedImageMapImage> {
@Override
public void save(final BufferedImageMapImage image, final OutputStream out) throws IOException {
final ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
try (final ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(out)) {
writer.setOutput(imageOutputStream);
final ImageWriteParam param = writer.getDefaultWriteParam();
if (Config.COMPRESS_IMAGES && param.canWriteCompressed()) {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
if (param.getCompressionType() == null) {
param.setCompressionType(param.getCompressionTypes()[0]);
}
param.setCompressionQuality(Config.COMPRESSION_RATIO);
}
writer.write(null, new IIOImage(image.image(), null, null), param);
}
}

@Override
public BufferedImageMapImage load(final Path input) throws IOException {
final @Nullable BufferedImage read = ImageIO.read(input.toFile());
if (read == null) {
throw new IOException("Failed to read image file '" + input.toAbsolutePath() + "', ImageIO.read(File) result is null. This means no " +
"supported image format was able to read it. The image file may have been malformed or corrupted, it will be overwritten.");
}
return new BufferedImageMapImage(read);
}

@Override
public BufferedImageMapImage newImage() {
return new BufferedImageMapImage(
new BufferedImage(MapImage.SIZE, MapImage.SIZE, BufferedImage.TYPE_INT_ARGB)
);
}

public record BufferedImageMapImage(BufferedImage image) implements MapImageIO.IOMapImage {
@Override
public void setPixel(final int x, final int y, final int color) {
this.image.setRGB(x, y, color);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
package xyz.jpenilla.squaremap.common.data;
package xyz.jpenilla.squaremap.common.data.image;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import net.minecraft.util.Mth;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
import xyz.jpenilla.squaremap.common.Logging;
import xyz.jpenilla.squaremap.common.config.Config;
import xyz.jpenilla.squaremap.common.config.Messages;
import xyz.jpenilla.squaremap.common.data.RegionCoordinate;
import xyz.jpenilla.squaremap.common.util.FileUtil;

@DefaultQualifier(NonNull.class)
public final class Image {
public final class MapImage {
private static final int TRANSPARENT = new Color(0, 0, 0, 0).getRGB();
public static final int SIZE = 512;
private final MapImageIO<MapImageIO.IOMapImage> backend;
private final RegionCoordinate region;
private final Path directory;
private final int maxZoom;
private int @Nullable [][] pixels = null;

public Image(final RegionCoordinate region, final Path directory, final int maxZoom) {
@SuppressWarnings("unchecked")
public MapImage(
final RegionCoordinate region,
final Path directory,
final int maxZoom,
final MapImageIO<?> backend
) {
this.region = region;
this.directory = directory;
this.maxZoom = maxZoom;
this.backend = (MapImageIO<MapImageIO.IOMapImage>) backend;
}

public synchronized void setPixel(final int x, final int z, final int color) {
Expand All @@ -59,7 +61,7 @@ public synchronized void save() {
int scaledX = Mth.floor((double) this.region.x() / step);
int scaledZ = Mth.floor((double) this.region.z() / step);

final BufferedImage image = this.getOrCreate(this.maxZoom - zoom, scaledX, scaledZ);
final MapImageIO.IOMapImage image = this.getOrCreate(this.maxZoom - zoom, scaledX, scaledZ);

int baseX = (this.region.x() * size) & (SIZE - 1);
int baseZ = (this.region.z() * size) & (SIZE - 1);
Expand All @@ -68,69 +70,48 @@ public synchronized void save() {
final int pixel = this.pixels[x][z];
if (pixel != Integer.MIN_VALUE) {
final int color = pixel == 0 ? TRANSPARENT : pixel;
image.setRGB(baseX + (x / step), baseZ + (z / step), color);
image.setPixel(baseX + (x / step), baseZ + (z / step), color);
}
}
}

this.save(this.maxZoom - zoom, scaledX, scaledZ, image);
this.saveImage(this.maxZoom - zoom, scaledX, scaledZ, image);
}
}

private BufferedImage getOrCreate(final int zoom, final int scaledX, final int scaledZ) {
private MapImageIO.IOMapImage getOrCreate(final int zoom, final int scaledX, final int scaledZ) {
final Path file = this.imageInDirectory(zoom, scaledX, scaledZ);

if (!Files.isRegularFile(file)) {
return newBufferedImage();
return this.backend.newImage();
}

try {
final @Nullable BufferedImage read = ImageIO.read(file.toFile());
if (read == null) {
throw new IOException("Failed to read image file '" + file.toAbsolutePath() + "', ImageIO.read(File) result is null. This means no " +
"supported image format was able to read it. The image file may have been malformed or corrupted, it will be overwritten.");
}
return read;
return this.backend.load(file);
} catch (final IOException ex) {
try {
Files.deleteIfExists(file);
} catch (final IOException ex0) {
ex.addSuppressed(ex0);
}
this.logCouldNotRead(ex);
return newBufferedImage();
return this.backend.newImage();
}
}

private void save(final int zoom, final int scaledX, final int scaledZ, final BufferedImage image) {
private void saveImage(final int zoom, final int scaledX, final int scaledZ, final MapImageIO.IOMapImage image) {
final Path out = this.imageInDirectory(zoom, scaledX, scaledZ);
try {
FileUtil.atomicWrite(out, tmp -> {
try (final OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(tmp))) {
save(image, outputStream);
this.backend.save(image, outputStream);
}
});
} catch (final IOException ex) {
this.logCouldNotSave(ex);
}
}

private static void save(final BufferedImage image, final OutputStream out) throws IOException {
final ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
try (final ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(out)) {
writer.setOutput(imageOutputStream);
final ImageWriteParam param = writer.getDefaultWriteParam();
if (Config.COMPRESS_IMAGES && param.canWriteCompressed()) {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
if (param.getCompressionType() == null) {
param.setCompressionType(param.getCompressionTypes()[0]);
}
param.setCompressionQuality(Config.COMPRESSION_RATIO);
}
writer.write(null, new IIOImage(image, null, null), param);
}
}

private Path imageInDirectory(final int zoom, final int scaledX, final int scaledZ) {
final Path dir = this.directory.resolve(Integer.toString(zoom));
if (!Files.exists(dir)) {
Expand All @@ -144,10 +125,6 @@ private Path imageInDirectory(final int zoom, final int scaledX, final int scale
return dir.resolve(fileName);
}

private static BufferedImage newBufferedImage() {
return new BufferedImage(Image.SIZE, Image.SIZE, BufferedImage.TYPE_INT_ARGB);
}

private void logCouldNotRead(final IOException ex) {
Logging.logger().error(xz(Messages.LOG_COULD_NOT_READ_REGION), ex);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package xyz.jpenilla.squaremap.common.data.image;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;

@DefaultQualifier(NonNull.class)
public interface MapImageIO<I extends MapImageIO.IOMapImage> {
void save(I image, OutputStream out) throws IOException;

I load(Path input) throws IOException;

I newImage();

interface IOMapImage {
void setPixel(int x, int y, int color);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package xyz.jpenilla.squaremap.common.data.image;

import io.github.xfacthd.pnj.api.PNJ;
import io.github.xfacthd.pnj.api.data.Image;
import io.github.xfacthd.pnj.api.define.ColorFormat;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;

@DefaultQualifier(NonNull.class)
public final class PNJMapImageIO implements MapImageIO<PNJMapImageIO.PNJMapImage> {

@Override
public void save(final PNJMapImage image, final OutputStream out) throws IOException {
PNJ.encode(out, image.image());
}

@Override
public PNJMapImage load(final Path input) throws IOException {
return new PNJMapImage(PNJ.decode(input));
}

@Override
public PNJMapImage newImage() {
return new PNJMapImage(
new Image(
MapImage.SIZE,
MapImage.SIZE,
ColorFormat.RGB_ALPHA,
8,
new byte[MapImage.SIZE * MapImage.SIZE * 4]
)
);
}

public record PNJMapImage(Image image) implements IOMapImage {
@Override
public void setPixel(int x, int y, int color) {
this.image.setPixel(x, y, color, true);
}
}
}
Loading