diff --git a/src/main/java/btpos/tools/mclowrespackgenerator/ArgHandler.java b/src/main/java/btpos/tools/mclowrespackgenerator/ArgHandler.java index 031faf0..52b960f 100644 --- a/src/main/java/btpos/tools/mclowrespackgenerator/ArgHandler.java +++ b/src/main/java/btpos/tools/mclowrespackgenerator/ArgHandler.java @@ -18,6 +18,19 @@ import java.util.stream.Collectors; public class ArgHandler { + private final boolean isHeadless; + + public ArgHandler(boolean isHeadless) { + this.isHeadless = isHeadless; + } + + public Args getArgs(String[] args) { + if (isHeadless) + return checkCliArgs(args); + else + return getUIArgs(); + } + public Args checkCliArgs(String[] args) throws ParseException { Option inputFilesOption = Option.builder("i") .required() @@ -35,12 +48,13 @@ public Args checkCliArgs(String[] args) throws ParseException { .type(File.class) .build(); - Option widthOption = Option.builder("m") - .longOpt("max-scale") - .desc("Maximum size (in pixels) of block and item textures. Accounts for animated textures.\nIf unset, downscales to the minimum size required for the GPU to be able to load all textures.") - .hasArg() - .type(Integer.class) - .build(); + Option sizeOption = Option.builder("s") + .longOpt("max-size") + .desc("Maximum size (in pixels) of block and item textures. Accounts for animated textures.\n" + + "If unset, downscales all textures to the minimum size required for the GPU to be able to load the texture atlas.") + .hasArg() + .type(Integer.class) + .build(); Option formatOption = Option.builder("p") .longOpt("pack-format") @@ -51,16 +65,14 @@ public Args checkCliArgs(String[] args) throws ParseException { Options o = new Options().addOption(inputFilesOption) .addOption(outputFileOption) - .addOption(widthOption) + .addOption(sizeOption) .addOption(formatOption); CommandLine parsed = new DefaultParser().parse(o, args); return new Args( - parsed.getParsedOptionValue(widthOption), - Optional.ofNullable(parsed.getOptionValues(inputFilesOption)) - .map(names -> Arrays.stream(names).map(File::new).collect(Collectors.toList())) - .orElse(null), + parsed.getParsedOptionValue(sizeOption, (Integer) null), + Arrays.stream(parsed.getOptionValues(inputFilesOption)).map(File::new).collect(Collectors.toList()), parsed.getParsedOptionValue(outputFileOption, new File("downscaled.zip")) ); } @@ -70,20 +82,13 @@ public Args getUIArgs() { File outputFile = getOutputFile(); - int autoscale = JOptionPane.showOptionDialog( - null, - "Autoscale textures?", - "", - JOptionPane.YES_NO_OPTION, - JOptionPane.PLAIN_MESSAGE, - null, - null, - null - ); + int autoscale = JOptionPane.showConfirmDialog(null, "Autoscale textures?"); Integer maxScale = null; - if (autoscale != JOptionPane.YES_OPTION) { + if (autoscale == JOptionPane.NO_OPTION) { maxScale = getWidth(); + } else if (autoscale == JOptionPane.CANCEL_OPTION) { + System.exit(0); } return new Args(maxScale, inputFiles, outputFile); diff --git a/src/main/java/btpos/tools/mclowrespackgenerator/Main.java b/src/main/java/btpos/tools/mclowrespackgenerator/Main.java index 10c8bb5..72e0b94 100644 --- a/src/main/java/btpos/tools/mclowrespackgenerator/Main.java +++ b/src/main/java/btpos/tools/mclowrespackgenerator/Main.java @@ -5,11 +5,7 @@ import org.apache.commons.io.file.PathUtils; import javax.imageio.ImageIO; -import javax.swing.JFrame; -import javax.swing.JOptionPane; -import javax.swing.UIManager; import java.awt.Dimension; -import java.awt.EventQueue; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -36,7 +32,8 @@ public class Main { - private Args args; + private static final Path outDirectory = Files.createTempDirectory("mcdownscaler"); + private static Args args; private static final boolean IS_HEADLESS = java.awt.GraphicsEnvironment.isHeadless(); @@ -44,42 +41,22 @@ public class Main { private static final String packmcmeta = "{\n\t\"pack\":{\n\t\t\"pack_format\": 15,\n\t\t \"description\": \"Compressed textures\"\n\t}\n}"; + private static final UserInterfaceHandler uiHandler = UserInterfaceHandler.get(IS_HEADLESS); public static void main(String[] argsIn) { - try { - if (System.getProperty("os.name").toLowerCase().contains("windows")) - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception ignored) { - } + uiHandler.onStart(); + args = new ArgHandler(IS_HEADLESS).getArgs(argsIn); - JFrame jFrame; - if (!IS_HEADLESS) { - jFrame = UIGenerators.getConsolePanel(); - - EventQueue.invokeLater(() -> jFrame.setVisible(true)); - } else { - jFrame = null; + if (args.outputFile.exists()) { + args.outputFile.createNewFile(); } - new Main().fileLogic(argsIn); - - if (jFrame != null) - jFrame.dispose(); + doCompression(); } - public void fileLogic(String[] argsIn) { - ArgHandler argHandler = new ArgHandler(); - if (IS_HEADLESS) - args = argHandler.checkCliArgs(argsIn); - else - args = argHandler.getUIArgs(); - - args.outputFile.createNewFile(); - - final Path outDirectory = Files.createTempDirectory("mcdownscaler"); - + public static void doCompression() { try { List>> name_to_stream = args.inputFiles.stream() .map(file -> catcher(() -> new ZipFile(file), null, e -> Util.fileBad(file.getName(), e))) @@ -92,66 +69,41 @@ public void fileLogic(String[] argsIn) { }) .collect(Collectors.toList()); - PriorityQueue>>> texturesHeap = new PriorityQueue<>(Comparator.comparingInt(Pair>>::getLeft).reversed()); + // max heap of total size in pixels + PriorityQueue texturesHeap = new PriorityQueue<>(Comparator.comparingInt(TextureEntry::getTotalSize).reversed()); AtomicInteger totalSize = new AtomicInteger(0); name_to_stream.stream() - .map(name_stream -> { - Dimension pngDimension = catcher(() -> Util.getPngDimension(name_stream.left, name_stream.right.get()), null, (e) -> fileBad(name_stream.left, e)); - if (pngDimension == null) - return null; - return new Pair<>(pngDimension, name_stream); - }).filter(Objects::nonNull) - .map(dim_pair -> dim_pair.lmap(dim_pair.left.width * dim_pair.left.height)) - .peek(size_pair -> totalSize.addAndGet(size_pair.left)) + .map(str_supp -> new TextureEntry(str_supp.left, str_supp.right)) + .peek(entry -> totalSize.addAndGet(entry.getTotalSize())) .forEach(texturesHeap::add); boolean hasOperated = false; - while (!texturesHeap.isEmpty() && totalSize.get() >= MAX_ATLAS_SIZE) { + System.out.println("Starting combined texture size (pixels): " + totalSize.get()); + + + while (!texturesHeap.isEmpty() && (!isAutoscale() || totalSize.get() >= MAX_ATLAS_SIZE)) { hasOperated = true; - Pair>> tuple = texturesHeap.poll(); - Integer oldSize = tuple.left; - String name = tuple.right.left; - Supplier is_get = tuple.right.right; + TextureEntry oldEntry = texturesHeap.poll(); + int oldSize = oldEntry.getTotalSize(); + TextureEntry newEntry; try { - BufferedImage img = ImageIO.read(is_get.get()); - if (img == null) { - System.err.println("Image null somehow idk"); - continue; - } - - int square = Util.getClosestPowerOf2(Math.max(img.getWidth(), img.getHeight())); - - System.out.println("Compressing: " + name); - img = Thumbnailator.createThumbnail(img, square, square); - - int newSize = img.getHeight() * img.getWidth(); - totalSize.addAndGet(newSize - oldSize); // update total size - - - Path outPath = outDirectory.resolve(Paths.get(name)); - FileUtils.createParentDirectories(outPath.toFile()); - - try (OutputStream outFile = Files.newOutputStream(outPath)) { - ImageIO.write(img, PathUtils.getExtension(outPath), outFile); - } - - texturesHeap.add(new Pair<>( - newSize, - new Pair<>( - name, - () -> catcher(() -> Files.newInputStream(outPath), null, e -> fileBad(outPath.toString(), e)) - ) - )); - - } catch (IOException e) { - fileBad(name, e); + newEntry = compressTexture(oldEntry); + } catch (Exception e) { + fileBad(oldEntry.path, e); + continue; } + + totalSize.addAndGet(newEntry.getTotalSize() - oldSize); // update total size + + if (isAutoscale()) + texturesHeap.add(newEntry); // put back on the heap for possible re-resizing if needed } + String displayText; if (hasOperated) { File mcmeta = outDirectory.resolve("pack.mcmeta").toFile(); @@ -163,13 +115,45 @@ public void fileLogic(String[] argsIn) { displayText = "Nothing to do!"; } - EventQueue.invokeAndWait(() -> JOptionPane.showMessageDialog(null, displayText)); + uiHandler.onFinish(displayText); } catch (Exception e) { throw new RuntimeException(e); } - } + private static TextureEntry compressTexture(TextureEntry entry) { + try (InputStream is = entry.getter.get()) { + BufferedImage img = ImageIO.read(is); + if (img == null) { + throw new IOException("Image read as null: " + entry.path); + } + + int square = isAutoscale() ? entry.getBestCap() : args.maxScale; + + System.out.println("Compressing: " + entry.path); + img = Thumbnailator.createThumbnail(img, square, square); // does keep the aspect ratio the same + + + Path outPath = outDirectory.resolve(Paths.get(entry.path)); + FileUtils.createParentDirectories(outPath.toFile()); + + try (OutputStream outFile = Files.newOutputStream(outPath)) { + ImageIO.write(img, PathUtils.getExtension(outPath), outFile); + } + + return new TextureEntry( + entry.path, + () -> catcher(() -> Files.newInputStream(outPath), null, e -> fileBad(outPath.toString(), e)), + new Dimension(img.getWidth(), img.getHeight()) + ); + } + } + + private static boolean isAutoscale() { + return args.maxScale == null; + } + + public static void pack(Path sourceDirPath, String zipFilePath) throws IOException { File f = new File(zipFilePath); if (!f.exists()) diff --git a/src/main/java/btpos/tools/mclowrespackgenerator/TextAreaOutputStream.java b/src/main/java/btpos/tools/mclowrespackgenerator/TextAreaOutputStream.java index 0452203..31da76a 100644 --- a/src/main/java/btpos/tools/mclowrespackgenerator/TextAreaOutputStream.java +++ b/src/main/java/btpos/tools/mclowrespackgenerator/TextAreaOutputStream.java @@ -176,6 +176,7 @@ public synchronized void run() { if (don >= 100) { break; } + textArea.setCaretPosition(textArea.getDocument().getLength()); } if (don == lines.size()) { lines.clear(); diff --git a/src/main/java/btpos/tools/mclowrespackgenerator/TextureEntry.java b/src/main/java/btpos/tools/mclowrespackgenerator/TextureEntry.java new file mode 100644 index 0000000..32198ec --- /dev/null +++ b/src/main/java/btpos/tools/mclowrespackgenerator/TextureEntry.java @@ -0,0 +1,45 @@ +package btpos.tools.mclowrespackgenerator; + + +import java.awt.Dimension; +import java.io.InputStream; +import java.util.function.Supplier; + +public class TextureEntry { + public final String path; + public final Supplier getter; + public final Dimension dimension; + + public TextureEntry(String path, Supplier getter) { + this(path, getter, null); + } + + public TextureEntry(String path, Supplier getter, Dimension dimension) { + this.path = path; + this.getter = getter; + this.dimension = dimension != null ? dimension : findDimension(path, getter); + } + + private static Dimension findDimension(String path, Supplier getter) { + return Util.getPngDimension(path, getter.get()); + } + + public int getTotalSize() { + return dimension.height * dimension.width; + } + + public int getBestCap() { + int bigger = Math.max(dimension.height, dimension.width); + int smaller = Math.min(dimension.height, dimension.width); + + // handle animated textures that are one block stretched over many + if (bigger != smaller + && bigger % smaller == 0) + { + int ratio = bigger / smaller; + return Util.getClosestPowerOf2(smaller) * ratio; + } else { + return Util.getClosestPowerOf2(bigger); + } + } +} diff --git a/src/main/java/btpos/tools/mclowrespackgenerator/UIGenerators.java b/src/main/java/btpos/tools/mclowrespackgenerator/UIGenerators.java index 1d4a62f..710bc4f 100644 --- a/src/main/java/btpos/tools/mclowrespackgenerator/UIGenerators.java +++ b/src/main/java/btpos/tools/mclowrespackgenerator/UIGenerators.java @@ -3,19 +3,20 @@ import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTextArea; +import javax.swing.text.DefaultCaret; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; public class UIGenerators { - static JFrame getConsolePanel() { + public static JFrame enableConsolePanel() { JFrame jFrame = new JFrame(); jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JTextArea jTextArea = new JTextArea(); JScrollPane jsp = new JScrollPane(jTextArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - jsp.getVerticalScrollBar().addAdjustmentListener(e -> jTextArea.select(jTextArea.getHeight() + 1000, 0)); jTextArea.setEditable(false); + TextAreaOutputStream tex = new TextAreaOutputStream(jTextArea); PrintStream splitConsoleStream = new PrintStream(new OutputStream() { @@ -32,7 +33,7 @@ public void write(int b) throws IOException { System.setErr(splitConsoleStream); jFrame.setSize(500, 300); - jFrame.add(jTextArea); + jFrame.add(jsp); jFrame.setLocationRelativeTo(null); return jFrame; diff --git a/src/main/java/btpos/tools/mclowrespackgenerator/UserInterfaceHandler.java b/src/main/java/btpos/tools/mclowrespackgenerator/UserInterfaceHandler.java new file mode 100644 index 0000000..163a2bb --- /dev/null +++ b/src/main/java/btpos/tools/mclowrespackgenerator/UserInterfaceHandler.java @@ -0,0 +1,48 @@ +package btpos.tools.mclowrespackgenerator; + +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.UIManager; +import java.awt.EventQueue; + +public interface UserInterfaceHandler { + static UserInterfaceHandler get(boolean isHeadless) { + try { + if (System.getProperty("os.name").toLowerCase().contains("windows")) + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception ignored) {} + + if (isHeadless) + return new Headless(); + else + return new Graphical(); + } + + default void onStart() {} + + void onFinish(String message); + + final class Graphical implements UserInterfaceHandler { + private JFrame consoleFrame; + + @Override + public void onStart() { + consoleFrame = UIGenerators.enableConsolePanel(); + + EventQueue.invokeLater(() -> consoleFrame.setVisible(true)); + } + + @Override + public void onFinish(String message) { + EventQueue.invokeAndWait(() -> JOptionPane.showMessageDialog(null, message)); + consoleFrame.dispose(); + } + } + + final class Headless implements UserInterfaceHandler { + @Override + public void onFinish(String message) { + System.out.println(message); + } + } +} diff --git a/src/main/java/btpos/tools/mclowrespackgenerator/Util.java b/src/main/java/btpos/tools/mclowrespackgenerator/Util.java index 44a5842..cbdee4e 100644 --- a/src/main/java/btpos/tools/mclowrespackgenerator/Util.java +++ b/src/main/java/btpos/tools/mclowrespackgenerator/Util.java @@ -52,6 +52,7 @@ public static T catcher(Supplier canThrow, T ifThrows, Consumer T catcher(Supplier canThrow, T ifThrows, Consumer iter = ImageIO.getImageReadersBySuffix("png"); // TODO can I optimize this to not have to search every single time - while(iter.hasNext()) { + while (iter.hasNext()) { ImageReader reader = iter.next(); try { @@ -80,27 +81,27 @@ public static Dimension getPngDimension(String name, InputStream is) throws IOEx throw new IOException("Not a known image file: " + name); } - static boolean isTexture(String name) { + public static boolean isTexture(String name) { return ASSETS_PATTERN.matcher(name).find() && name.endsWith(".png"); } - static void fileBad(ZipEntry entry, Exception e) { + public static void fileBad(ZipEntry entry, Exception e) { fileBad(entry.getName(), e); } - static void fileBad(Pair entry, Exception e) { + public static void fileBad(Pair entry, Exception e) { fileBad(entry.left.getName(), e); } - static void fileBad(String name, Exception e) { + public static void fileBad(String name, Exception e) { System.out.println("Error reading file: " + name); e.printStackTrace(System.err); } - static int getClosestPowerOf2(final int i) { + public static int getClosestPowerOf2(final int i) { // there's a better way to do this but idc enough int out = 2; - while (out < i) { + while (i > out) { out *= 2; } return out / 2;