Image library initial implementation

*Moves the BufferedImage loader into the library, rather than being
a loader implemented in common/implementation
This commit is contained in:
Astrash
2022-08-12 11:08:16 +10:00
parent 1f7c4ee4e7
commit 3580267532
23 changed files with 445 additions and 22 deletions

View File

@@ -0,0 +1,8 @@
version = version("1.0.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
}

View File

@@ -0,0 +1,49 @@
package com.dfsek.terra.addons.image;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.awt.image.BufferedImage;
import java.util.function.Supplier;
import com.dfsek.terra.addons.image.config.BufferedImageLoader;
import com.dfsek.terra.addons.image.config.picker.SingleColorPickerTemplate;
import com.dfsek.terra.addons.image.config.picker.TileColorPickerTemplate;
import com.dfsek.terra.addons.image.picker.ColorPicker;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
import com.dfsek.terra.api.inject.annotations.Inject;
import com.dfsek.terra.api.registry.CheckedRegistry;
import com.dfsek.terra.api.util.reflection.TypeKey;
public class ImageLibraryAddon implements AddonInitializer {
public static final TypeKey<Supplier<ObjectTemplate<ColorPicker>>> COLOR_PICKER_REGISTRY_KEY = new TypeKey<>() {
};
@Inject
private Platform platform;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
platform.getEventManager()
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.priority(10)
.then(event -> {
event.getPack().applyLoader(BufferedImage.class, new BufferedImageLoader(event.getPack().getLoader()));
})
.then(event -> {
CheckedRegistry<Supplier<ObjectTemplate<ColorPicker>>> colorPickerRegistry = event.getPack().getOrCreateRegistry(
COLOR_PICKER_REGISTRY_KEY);
colorPickerRegistry.register(addon.key("SINGLE"), SingleColorPickerTemplate::new);
colorPickerRegistry.register(addon.key("TILED"), TileColorPickerTemplate::new);
});
}
}

View File

@@ -1,21 +1,4 @@
/*
* This file is part of Terra.
*
* Terra 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.
*
* Terra 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 Terra. If not, see <https://www.gnu.org/licenses/>.
*/
package com.dfsek.terra.config.loaders.config;
package com.dfsek.terra.addons.image.config;
import com.dfsek.tectonic.api.depth.DepthTracker;
import com.dfsek.tectonic.api.exception.LoadException;

View File

@@ -0,0 +1,16 @@
package com.dfsek.terra.addons.image.config.converter;
import com.dfsek.terra.addons.image.converter.ClosestMatchColorConverter;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
public abstract class ClosestColorConverterTemplate<T> implements ColorConverterTemplate<T> {
protected abstract ColorMapping<T> getMapping();
@Override
public ColorConverter<T> get() {
return new ClosestMatchColorConverter<T>(getMapping().get());
}
}

View File

@@ -0,0 +1,9 @@
package com.dfsek.terra.addons.image.config.converter;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.converter.ColorConverter;
public interface ColorConverterTemplate<T> extends ObjectTemplate<ColorConverter<T>> {
}

View File

@@ -0,0 +1,18 @@
package com.dfsek.terra.addons.image.config.converter;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.converter.ExactColorConverter;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
public abstract class ExactColorConverterTemplate<T> implements ColorConverterTemplate<T> {
protected abstract ColorMapping<T> getMapping();
protected abstract T getFallback();
@Override
public ColorConverter<T> get() {
return new ExactColorConverter<T>(getMapping().get(), getFallback());
}
}

View File

@@ -0,0 +1,17 @@
package com.dfsek.terra.addons.image.config.picker;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.picker.ColorPicker;
import com.dfsek.terra.addons.image.picker.transform.Alignment;
public abstract class ColorPickerTemplate implements ObjectTemplate<ColorPicker> {
@Value("align")
@Default
protected Alignment alignment = Alignment.NONE;
}

View File

@@ -0,0 +1,18 @@
package com.dfsek.terra.addons.image.config.picker;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.image.picker.ColorPicker;
import com.dfsek.terra.addons.image.picker.SimpleColorPicker;
public class SingleColorPickerTemplate extends ColorPickerTemplate {
@Value("fallback")
private int fallback;
@Override
public ColorPicker get() {
return new SimpleColorPicker(fallback, alignment);
}
}

View File

@@ -0,0 +1,13 @@
package com.dfsek.terra.addons.image.config.picker;
import com.dfsek.terra.addons.image.picker.ColorPicker;
import com.dfsek.terra.addons.image.picker.TileColorPicker;
public class TileColorPickerTemplate extends ColorPickerTemplate {
@Override
public ColorPicker get() {
return new TileColorPicker(alignment);
}
}

View File

@@ -0,0 +1,39 @@
package com.dfsek.terra.addons.image.converter;
import java.util.Map;
import com.dfsek.terra.addons.image.util.ColorUtil;
public class ClosestMatchColorConverter<T> implements ColorConverter<T> {
private final Map<Integer, T> map;
private final Integer[] colors;
public ClosestMatchColorConverter(Map<Integer, T> map) {
this.map = map;
this.colors = map.keySet().toArray(new Integer[0]);
}
@Override
public T apply(Integer color) {
int closest = 0;
int smallestDistance = Integer.MAX_VALUE;
for(int compare : colors) {
int distance = ColorUtil.distance(color, compare);
if(distance == 0) {
closest = compare;
break;
} else if(distance < smallestDistance) {
smallestDistance = distance;
closest = compare;
}
}
return map.get(closest);
}
@Override
public Iterable<T> getEntries() {
return map.values();
}
}

View File

@@ -0,0 +1,8 @@
package com.dfsek.terra.addons.image.converter;
import java.util.function.Function;
public interface ColorConverter<T> extends Function<Integer, T> {
Iterable<T> getEntries();
}

View File

@@ -0,0 +1,31 @@
package com.dfsek.terra.addons.image.converter;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class ExactColorConverter<T> implements ColorConverter<T> {
private final Map<Integer, T> map;
private final T fallback;
public ExactColorConverter(Map<Integer, T> map, T fallback) {
this.map = map;
this.fallback = fallback;
}
@Override
public T apply(Integer color) {
T lookup = map.get(color);
if(lookup != null) return lookup;
return fallback;
}
@Override
public Iterable<T> getEntries() {
Set<T> entries = new HashSet<>(map.values());
entries.add(fallback);
return entries;
}
}

View File

@@ -0,0 +1,38 @@
package com.dfsek.terra.addons.image.converter.mapping;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.dfsek.terra.api.registry.Registry;
import com.dfsek.terra.api.world.biome.Biome;
public class BiomeDefinedColorMapping<T> implements ColorMapping<T> {
Registry<Biome> biomeRegistry;
Function<Biome, T> converter;
public BiomeDefinedColorMapping(Registry<Biome> biomeRegistry, Function<Biome, T> converter) {
this.biomeRegistry = biomeRegistry;
this.converter = converter;
}
@Override
public Map<Integer, T> get() {
Map<Biome, Integer> colorMap = new HashSet<>(biomeRegistry.entries()).stream().collect(Collectors.toMap(b -> b, Biome::getColor));
Map<Integer, Biome> output = new HashMap<>();
colorMap.forEach(((biome, color) -> {
if(!output.containsKey(color)) {
output.put(color, biome);
} else {
throw new IllegalArgumentException(String.format("Biome %s has same color as %s: %x", biome.getID(), output.get(color).getID(), color));
}
}));
return output.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> converter.apply(e.getValue())));
}
}

View File

@@ -0,0 +1,8 @@
package com.dfsek.terra.addons.image.converter.mapping;
import java.util.Map;
import java.util.function.Supplier;
public interface ColorMapping<T> extends Supplier<Map<Integer, T>> {
}

View File

@@ -0,0 +1,15 @@
package com.dfsek.terra.addons.image.picker;
import java.awt.image.BufferedImage;
@FunctionalInterface
public interface ColorPicker {
/**
* @param image Lookup image
* @param x World x coordinate
* @param z World z coordinate
* @return Integer representing a web color
*/
Integer apply(BufferedImage image, int x, int z);
}

View File

@@ -0,0 +1,26 @@
package com.dfsek.terra.addons.image.picker;
import java.awt.image.BufferedImage;
import com.dfsek.terra.addons.image.picker.transform.ImageTransformation;
public class SimpleColorPicker implements ColorPicker {
private final Integer fallback;
private final ImageTransformation transformation;
public SimpleColorPicker(int fallbackColor, ImageTransformation transformation) {
this.fallback = fallbackColor;
this.transformation = transformation;
}
@Override
public Integer apply(BufferedImage image, int x, int z) {
x = transformation.transformX(image, x);
z = transformation.transformZ(image, z);
if(x < 0 || z < 0 || x >= image.getWidth() || z >= image.getHeight()) return fallback;
return image.getRGB(x, z);
}
}

View File

@@ -0,0 +1,24 @@
package com.dfsek.terra.addons.image.picker;
import net.jafama.FastMath;
import java.awt.image.BufferedImage;
import com.dfsek.terra.addons.image.picker.transform.ImageTransformation;
public class TileColorPicker implements ColorPicker {
private final ImageTransformation transformation;
public TileColorPicker(ImageTransformation transformation) {
this.transformation = transformation;
}
@Override
public Integer apply(BufferedImage image, int x, int z) {
x = transformation.transformX(image, x);
z = transformation.transformZ(image, z);
return image.getRGB(FastMath.floorMod(x, image.getWidth()), FastMath.floorMod(z, image.getHeight()));
}
}

View File

@@ -0,0 +1,30 @@
package com.dfsek.terra.addons.image.picker.transform;
import java.awt.image.BufferedImage;
public enum Alignment implements ImageTransformation {
NONE() {
@Override
public int transformX(BufferedImage image, int x) {
return x;
}
@Override
public int transformZ(BufferedImage image, int z) {
return z;
}
},
CENTER {
@Override
public int transformX(BufferedImage image, int x) {
return x + image.getWidth() / 2;
}
@Override
public int transformZ(BufferedImage image, int z) {
return z + image.getHeight() / 2;
}
};
}

View File

@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.image.picker.transform;
import java.awt.image.BufferedImage;
public interface ImageTransformation {
int transformX(BufferedImage image, int x);
int transformZ(BufferedImage image, int z);
}

View File

@@ -0,0 +1,30 @@
package com.dfsek.terra.addons.image.util;
import net.jafama.FastMath;
public class ColorUtil {
private ColorUtil() {}
public static int distance(int a, int b) {
return FastMath.abs(getRed(a) - getRed(b)) + FastMath.abs(getGreen(a) - getGreen(b)) + FastMath.abs(getBlue(a) - getBlue(b));
}
public static int getRed(int rgb) {
return rgb >> 16 & 255;
}
public static int getGreen(int rgb) {
return rgb >> 8 & 255;
}
public static int getBlue(int rgb) {
return rgb >> 0 & 255;
}
public static int getAlpha(int rgb) {
return rgb >> 24 & 255;
}
}

View File

@@ -0,0 +1,23 @@
package com.dfsek.terra.addons.image.util;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
public class MapUtil {
private MapUtil() {}
/**
* Utility method for converting config keys into integers,
* used because Tectonic does not accept non string keys.
*/
public static <T> Map<Integer, T> convertKeysToInt(Map<String, T> map) {
return map.entrySet().stream()
.collect(Collectors.toMap(
e -> Integer.decode(e.getKey()),
Entry::getValue
));
}
}

View File

@@ -0,0 +1,12 @@
schema-version: 1
contributors:
- Terra contributors
id: library-image
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addons.image.ImageLibraryAddon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License

View File

@@ -32,7 +32,6 @@ import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -82,7 +81,6 @@ import com.dfsek.terra.api.world.chunk.generation.util.provider.ChunkGeneratorPr
import com.dfsek.terra.config.fileloaders.FolderLoader;
import com.dfsek.terra.config.fileloaders.ZIPLoader;
import com.dfsek.terra.config.loaders.GenericTemplateSupplierLoader;
import com.dfsek.terra.config.loaders.config.BufferedImageLoader;
import com.dfsek.terra.config.preprocessor.MetaListLikePreprocessor;
import com.dfsek.terra.config.preprocessor.MetaMapPreprocessor;
import com.dfsek.terra.config.preprocessor.MetaNumberPreprocessor;
@@ -282,8 +280,7 @@ public class ConfigPackImpl implements ConfigPack {
@Override
public void register(TypeRegistry registry) {
registry.registerLoader(ConfigType.class, configTypeRegistry)
.registerLoader(BufferedImage.class, new BufferedImageLoader(loader));
registry.registerLoader(ConfigType.class, configTypeRegistry);
registryMap.forEach(registry::registerLoader);
shortcuts.forEach(registry::registerLoader); // overwrite with delegated shortcuts if present
}