package hunternif.mc.atlas.util;
import hunternif.mc.atlas.client.*;
import hunternif.mc.atlas.client.gui.ExportUpdateListener;
import hunternif.mc.atlas.core.DimensionData;
import hunternif.mc.atlas.marker.DimensionMarkersData;
import hunternif.mc.atlas.marker.Marker;
import hunternif.mc.atlas.marker.MarkersData;
import hunternif.mc.atlas.registry.MarkerRegistry;
import hunternif.mc.atlas.registry.MarkerRenderInfo;
import hunternif.mc.atlas.registry.MarkerType;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.List;
@SideOnly(Side.CLIENT)
public class ExportImageUtil {
public static final int TILE_SIZE = 16;
public static final int MARKER_SIZE = 32;
public static boolean isExporting = false;
private static Frame frame;
private static final JFileChooser chooser = new JFileChooser();
private static ExportUpdateListener getListener() {
return ExportUpdateListener.INSTANCE;
}
static {
chooser.setDialogTitle(I18n.format("gui.antiqueatlas.exportImage"));
chooser.setSelectedFile(new File("Atlas.png"));
chooser.setFileFilter(new FileFilter() {
@Override
public String getDescription() {
return "PNG Image";
}
@Override
public boolean accept(File file) {
// Accept all files so they are visible
return true;
}
});
}
/** Beware that the background texture doesn't follow the Autotile format. */
private static final int BG_TILE_SIZE = 22;
/** Opens a dialog and returns the file that was chosen, null if none or error. */
public static File selectPngFileToSave(String atlasName) {
getListener().setHeaderString("");
getListener().setStatusString("gui.antiqueatlas.export.opening");
getListener().setProgressMax(-1);
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
Log.error(e, "Setting system Look&Feel for JFileChooser");
}
getListener().setStatusString("gui.antiqueatlas.export.selectFile");
frame = new Frame();
if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
frame.dispose();
// Check file extension:
if (file.getName().length() < 4 || // No extension
!file.getName().substring(file.getName().length() - 4).equalsIgnoreCase(".png")) {
file = new File(file.getAbsolutePath() + ".png");
}
return file;
}
frame.dispose();
return null;
}
/** Renders the map into file as PNG image. */
public static void exportPngImage(DimensionData biomeData, DimensionMarkersData globalMarkers,
DimensionMarkersData localMarkers, File file, boolean showMarkers) {
getListener().setHeaderString("gui.antiqueatlas.export.setup");
// Prepare output image
// Leave padding of one row of map tiles on each side
int minX = (biomeData.getScope().minX - 1) * TILE_SIZE;
int minY = (biomeData.getScope().minY - 1) * TILE_SIZE;
int outWidth = (biomeData.getScope().maxX + 2) * TILE_SIZE - minX;
int outHeight = (biomeData.getScope().maxY + 2) * TILE_SIZE - minY;
Log.info("Image size: %dx%d", outWidth, outHeight);
getListener().setStatusString("gui.antiqueatlas.export.makingbuffer", outWidth, outHeight);
BufferedImage outImage = new BufferedImage(outWidth, outHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = outImage.createGraphics();
// Draw background, double scale:
int scale = 2;
int bgTilesX = Math.round((float)outWidth / (float)BG_TILE_SIZE / (float)scale);
int bgTilesY = Math.round((float)outHeight / (float)BG_TILE_SIZE / (float)scale);
// Count background tiles too:
// Preload all textures (they should be small enough)
// Count loaded textures as update units too.
getListener().setStatusString("gui.antiqueatlas.export.loadingtextures");
getListener().setProgressMax(-1);
BufferedImage bg = null;
Map<ResourceLocation, BufferedImage> textureImageMap = new HashMap<>();
try {
InputStream is = Minecraft.getMinecraft().getResourceManager().getResource(Textures.EXPORTED_BG).getInputStream();
bg = ImageIO.read(is);
is.close();
// Biome & Marker textures:
List<ResourceLocation> allTextures = new ArrayList<>(64);
allTextures.addAll(BiomeTextureMap.instance().getAllTextures());
if (showMarkers) {
for (MarkerType type : MarkerRegistry.getValues()) {
allTextures.addAll(Arrays.asList( type.getAllIcons() ));
// allTextures.add(type.getIcon());
}
}
for (ResourceLocation texture : allTextures) {
try {
is = Minecraft.getMinecraft().getResourceManager().getResource(texture).getInputStream();
BufferedImage tileImage = ImageIO.read(is);
is.close();
textureImageMap.put(texture, tileImage);
} catch (FileNotFoundException e) {
// This can happen, for example, when you remove a mod that has added custom textures
Log.warn("Texture %s not found!", texture.toString());
}
}
} catch (IOException e) {
e.printStackTrace();
}
getListener().setHeaderString("gui.antiqueatlas.export.rendering");
drawMapToGraphics(
graphics,
bgTilesX, bgTilesY, outWidth, outHeight,
biomeData, textureImageMap,
globalMarkers, localMarkers,
showMarkers, minX, minY,
scale, bg);
try {
getListener().setHeaderString("");
getListener().setStatusString("gui.antiqueatlas.export.writing");
ImageIO.write(outImage, "PNG", file);
Log.info("Done writing image");
} catch (IOException e) {
e.printStackTrace();
}
}
/** Renders the map into file as PNG image stripe by stripe in order to not have a OutOfMemoryError. */
public static void exportPngImageTooLarge(final DimensionData biomeData, final DimensionMarkersData globalMarkers,
final DimensionMarkersData localMarkers, File file, final boolean showMarkers) {
getListener().setHeaderString("");
// Prepare output image
// Leave padding of one row of map tiles on each side
final int minX = (biomeData.getScope().minX - 1) * TILE_SIZE;
final int minY = (biomeData.getScope().minY - 1) * TILE_SIZE;
final int outWidth = (biomeData.getScope().maxX + 2) * TILE_SIZE - minX;
final int outHeight = (biomeData.getScope().maxY + 2) * TILE_SIZE - minY;
Log.info("Image size: %dx%d", outWidth, outHeight);
// Draw background, double scale:
final int scale = 2;
final int bgTilesX = Math.round((float)outWidth / (float)BG_TILE_SIZE / (float)scale);
final int bgTilesY = Math.round((float)outHeight / (float)BG_TILE_SIZE / (float)scale);
// Preload all textures (they should be small enough)
// Count loaded textures as update units too.
getListener().setStatusString("gui.antiqueatlas.export.loadingtextures");
getListener().setProgressMax(-1);
BufferedImage bg = null;
final Map<ResourceLocation, BufferedImage> textureImageMap = new HashMap<>();
try {
InputStream is = Minecraft.getMinecraft().getResourceManager().getResource(Textures.EXPORTED_BG).getInputStream();
bg = ImageIO.read(is);
is.close();
// Biome & Marker textures:
List<ResourceLocation> allTextures = new ArrayList<>(64);
allTextures.addAll(BiomeTextureMap.instance().getAllTextures());
if (showMarkers) {
for (MarkerType type : MarkerRegistry.getValues()) {
allTextures.addAll(Arrays.asList( type.getAllIcons() ));
// allTextures.add(type.getIcon());
}
}
for (ResourceLocation texture : allTextures) {
try {
is = Minecraft.getMinecraft().getResourceManager().getResource(texture).getInputStream();
BufferedImage tileImage = ImageIO.read(is);
is.close();
textureImageMap.put(texture, tileImage);
} catch (FileNotFoundException e) {
// This can happen, for example, when you remove a mod that has added custom textures
Log.warn("Texture %s not found!", texture.toString());
}
}
} catch (IOException e) {
e.printStackTrace();
}
System.gc();
long availableMem = getAvailableMemory();
long usableMem = (long) (availableMem * 0.8); // leave some breathing room
int pixelSize = Integer.SIZE/8;
int sliceHeight = TILE_SIZE;
for (int i = bgTilesY; i > 0; i--) {
long usedMem = ( (long)( i*TILE_SIZE ) * outWidth * pixelSize );
if( usedMem <= usableMem ) {
sliceHeight = i*TILE_SIZE;
break;
} else {
Log.info("%d tiles tall is too big, %d > %d", i, usedMem, usableMem);
}
}
final int sliceHeight_ = sliceHeight;
final int slices = (int)Math.ceil((float)outHeight/(float)sliceHeight);
final BufferedImage bg_ = bg;
final BufferedImage scanBuffer = new BufferedImage(outWidth, sliceHeight, BufferedImage.TYPE_INT_ARGB);
getListener().setProgressMax(slices);
RenderedImage outImage = new RenderedImageScanned(outWidth, outHeight, scanBuffer, graphics -> {
int slice = (int)Math.floor( -graphics.getTransform().getTranslateY()/sliceHeight_ );
getListener().setProgress( slice );
getListener().setHeaderString("gui.antiqueatlas.export.renderstripe", slice+1, slices);
drawMapToGraphics(
graphics,
bgTilesX, bgTilesY, outWidth, outHeight,
biomeData, textureImageMap,
globalMarkers, localMarkers,
showMarkers, minX, minY,
scale, bg_);
getListener().setStatusString("gui.antiqueatlas.export.writestripe");
getListener().setProgressMax(sliceHeight_ * (slice+1) > outHeight ? outHeight - ( sliceHeight_ * slice ) : sliceHeight_);
}, value -> getListener().setProgress(value));
try {
getListener().setHeaderString("gui.antiqueatlas.export.renderstripe", 1, slices);
ImageIO.write(outImage, "PNG", file);
Log.info("Done writing image");
} catch (IOException e) {
e.printStackTrace();
}
}
private static long getAvailableMemory() {
Runtime runtime = Runtime.getRuntime();
long totalMemory = runtime.totalMemory(); // current heap allocated to the VM process
long freeMemory = runtime.freeMemory(); // out of the current heap, how much is free
long maxMemory = runtime.maxMemory(); // Max heap VM can use e.g. Xmx setting
long usedMemory = totalMemory - freeMemory; // how much of the current heap the VM is using
return maxMemory - usedMemory; // available memory i.e. Maximum heap size minus the current amount used
}
private static void drawMapToGraphics(Graphics2D graphics,
int bgTilesX, int bgTilesY, int outWidth, int outHeight,
DimensionData biomeData, Map<ResourceLocation, BufferedImage> textureImageMap,
DimensionMarkersData globalMarkers, DimensionMarkersData localMarkers,
boolean showMarkers, int minX, int minY,
int scale, BufferedImage bg) {
getListener().setStatusString("gui.antiqueatlas.export.rendering.background");
getListener().setProgressMax(bgTilesX * bgTilesY);
//================ Draw map background ================
// Top left corner:
graphics.drawImage(bg, 0, 0, BG_TILE_SIZE * scale, BG_TILE_SIZE * scale,
0, 0, BG_TILE_SIZE, BG_TILE_SIZE, null);
getListener().addProgress(1);
// Topmost row:
for (int x = 1; x < bgTilesX; x++) {
graphics.drawImage(bg, x*BG_TILE_SIZE*scale, 0, (x + 1)*BG_TILE_SIZE*scale, BG_TILE_SIZE*scale,
BG_TILE_SIZE, 0, BG_TILE_SIZE*2, BG_TILE_SIZE, null);
getListener().addProgress(1);
}
// Leftmost column:
for (int y = 1; y < bgTilesY; y++) {
graphics.drawImage(bg, 0, y*BG_TILE_SIZE*scale, BG_TILE_SIZE*scale, (y + 1)*BG_TILE_SIZE*scale,
0, BG_TILE_SIZE, BG_TILE_SIZE, BG_TILE_SIZE*2, null);
getListener().addProgress(1);
}
// Middle:
for (int x = 1; x < bgTilesX; x++) {
for (int y = 1; y < bgTilesY; y++) {
graphics.drawImage(bg,
x*BG_TILE_SIZE*scale, y*BG_TILE_SIZE*scale,
(x + 1)*BG_TILE_SIZE*scale, (y + 1)*BG_TILE_SIZE*scale,
BG_TILE_SIZE, BG_TILE_SIZE, BG_TILE_SIZE*2, BG_TILE_SIZE*2, null);
getListener().addProgress(1);
}
}
// Top right corner:
graphics.drawImage(bg, outWidth - BG_TILE_SIZE*scale, 0,
outWidth, BG_TILE_SIZE * scale,
BG_TILE_SIZE*2, 0, BG_TILE_SIZE*3, BG_TILE_SIZE, null);
getListener().addProgress(1);
// Rightmost column:
for (int y = 1; y < bgTilesY; y++) {
graphics.drawImage(bg,
outWidth - BG_TILE_SIZE*scale, y*BG_TILE_SIZE*scale,
outWidth, (y + 1)*BG_TILE_SIZE*scale,
BG_TILE_SIZE*2, BG_TILE_SIZE, BG_TILE_SIZE*3, BG_TILE_SIZE*2, null);
getListener().addProgress(1);
}
// Bottom left corner:
graphics.drawImage(bg, 0, outHeight - BG_TILE_SIZE*scale,
BG_TILE_SIZE*scale, outHeight,
0, BG_TILE_SIZE*2, BG_TILE_SIZE, BG_TILE_SIZE*3, null);
getListener().addProgress(1);
// Bottommost row:
for (int x = 1; x < bgTilesX; x++) {
graphics.drawImage(bg, x*BG_TILE_SIZE*scale, outHeight - BG_TILE_SIZE*scale,
(x + 1)*BG_TILE_SIZE*scale, outHeight,
BG_TILE_SIZE, BG_TILE_SIZE*2, BG_TILE_SIZE*2, BG_TILE_SIZE*3, null);
getListener().addProgress(1);
}
// Bottom right corner:
graphics.drawImage(bg, outWidth - BG_TILE_SIZE*scale, outHeight - BG_TILE_SIZE*scale,
outWidth, outHeight, BG_TILE_SIZE*2, BG_TILE_SIZE*2, BG_TILE_SIZE*3, BG_TILE_SIZE*3, null);
getListener().addProgress(1);
//============= Draw actual map tiles ==============
Rect scope = biomeData.getScope();
getListener().setStatusString("gui.antiqueatlas.export.rendering.map");
getListener().setProgressMax(scope.getHeight() * scope.getWidth());
TileRenderIterator iter = new TileRenderIterator(biomeData);
while(iter.hasNext()) {
SubTileQuartet subtiles = iter.next();
for (SubTile subtile : subtiles) {
if (subtile == null || subtile.tile == null) continue;
// Load tile texture
ResourceLocation texture = BiomeTextureMap.instance().getTexture(subtile.tile);
BufferedImage tileImage = textureImageMap.get(texture);
if (tileImage == null) continue;
graphics.drawImage(tileImage,
TILE_SIZE + subtile.x * TILE_SIZE / 2,
TILE_SIZE + subtile.y * TILE_SIZE / 2,
TILE_SIZE + (subtile.x + 1) * TILE_SIZE / 2,
TILE_SIZE + (subtile.y + 1) * TILE_SIZE / 2,
subtile.getTextureU() * TILE_SIZE / 2,
subtile.getTextureV() * TILE_SIZE / 2,
(subtile.getTextureU() + 1) * TILE_SIZE / 2,
(subtile.getTextureV() + 1) * TILE_SIZE / 2,
null);
}
getListener().addProgress(1);
}
//============== Draw markers ================
// Draw local markers on top of global markers
getListener().setStatusString("gui.antiqueatlas.export.rendering.markers");
getListener().setProgressMax(-1);
List<Marker> markers = new ArrayList<>();
for (int x = biomeData.getScope().minX / MarkersData.CHUNK_STEP;
x <= biomeData.getScope().maxX / MarkersData.CHUNK_STEP; x++) {
for (int z = biomeData.getScope().minY / MarkersData.CHUNK_STEP;
z <= biomeData.getScope().maxY / MarkersData.CHUNK_STEP; z++) {
markers.clear();
List<Marker> globalMarkersAt = globalMarkers.getMarkersAtChunk(x, z);
if (globalMarkersAt != null) {
markers.addAll(globalMarkers.getMarkersAtChunk(x, z));
}
if (localMarkers != null) {
List<Marker> localMarkersAt = localMarkers.getMarkersAtChunk(x, z);
if (localMarkersAt != null) {
markers.addAll(localMarkersAt);
}
}
for (Marker marker : markers) {
MarkerType type = marker.getType();
if (!marker.isVisibleAhead() &&
!biomeData.hasTileAt(marker.getChunkX(), marker.getChunkZ())) {
continue;
}
if (type.shouldHide(!showMarkers, 0)) {
continue;
}
type.calculateMip(1, 1, 1);
MarkerRenderInfo info = type.getRenderInfo(1, 1, 1);
type.resetMip();
// Load marker texture
ResourceLocation texture = info.tex;
BufferedImage markerImage = textureImageMap.get(texture);
if (markerImage == null)
continue;
int markerX = marker.getX() - minX;
int markerY = marker.getZ() - minY;
graphics.drawImage(
markerImage,
(int)( markerX + info.x), (int)( markerY + info.y ),
info.width, info.height, null);
}
}
}
}
}