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); } } } } }