diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..fb50116 --- /dev/null +++ b/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae3c172 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/.project b/.project new file mode 100644 index 0000000..a82ce5a --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + Factorio Screenshot Stitcher + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..99f26c0 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/src/anim.gif b/src/anim.gif new file mode 100644 index 0000000..256a4e0 Binary files /dev/null and b/src/anim.gif differ diff --git a/src/nbradham/stitcher/Screenshot.java b/src/nbradham/stitcher/Screenshot.java new file mode 100644 index 0000000..3479689 --- /dev/null +++ b/src/nbradham/stitcher/Screenshot.java @@ -0,0 +1,66 @@ +package nbradham.stitcher; + +import java.util.ArrayList; + +/** + * Handles tracking what files are related to which screenshots. + * + * @author Nickolas Bradham + * + */ +final class Screenshot { + + private final ArrayList splits = new ArrayList<>(); + + /** + * Add a split to this screenshot. + * + * @param split + */ + final void add(Split split) { + splits.add(split); + } + + /** + * Retrieves a Split from this screenshot. + * + * @param ind The index of the desired Split. + * @return The requested Split. + */ + final Split getSplit(int ind) { + return splits.get(ind); + } + + /** + * Retrieves how many Splits wide the screenshot is. + * + * @return The number of Splits across the x axis. + */ + final int lengthX() { + int res = 0; + for (Split s : splits) + res = Math.max(res, s.x()); + return res + 1; + } + + /** + * Retrieves how many Splits high the screenshot is. + * + * @return The number of Splits across the y axis. + */ + final int lengthY() { + int res = 0; + for (Split s : splits) + res = Math.max(res, s.y()); + return res + 1; + } + + /** + * Retrieves how many Splits this screenshot contains. + * + * @return The total number of splits in this screenshot. + */ + final int numSplits() { + return splits.size(); + } +} \ No newline at end of file diff --git a/src/nbradham/stitcher/Split.java b/src/nbradham/stitcher/Split.java new file mode 100644 index 0000000..5ac0d1f --- /dev/null +++ b/src/nbradham/stitcher/Split.java @@ -0,0 +1,12 @@ +package nbradham.stitcher; + +import java.io.File; + +/** + * Holds information of a individual split. + * + * @author Nickolas Bradham + * + */ +record Split(File file, byte x, byte y) { +} \ No newline at end of file diff --git a/src/nbradham/stitcher/Stitcher.java b/src/nbradham/stitcher/Stitcher.java new file mode 100644 index 0000000..db37486 --- /dev/null +++ b/src/nbradham/stitcher/Stitcher.java @@ -0,0 +1,166 @@ +package nbradham.stitcher; + +import java.awt.FlowLayout; +import java.awt.Graphics; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JProgressBar; +import javax.swing.SwingUtilities; + +/** + * Handles core execution. + * + * @author Nickolas Bradham + * + */ +final class Stitcher extends WindowAdapter { + + private final JFrame frame = new JFrame("Stitcher"); + private final JProgressBar bar = new JProgressBar(); + private final HashMap shots = new HashMap<>(); + private final Thread[] threads = new Thread[Runtime.getRuntime().availableProcessors()]; + + private Iterator shotIterator; + private boolean run = true; + + /** + * Displays GUI, handles loading, and handles stitching threads. + */ + private void start() { + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.setResizable(false); + frame.addWindowListener(this); + frame.setLayout(new FlowLayout()); + bar.setStringPainted(true); + frame.add(new JLabel("Status:")); + frame.add(new JLabel(new ImageIcon(Stitcher.class.getResource("/anim.gif")))); + frame.add(bar); + frame.pack(); + frame.setVisible(true); + + JFileChooser jfc = new JFileChooser( + Paths.get(System.getenv("appdata"), "Factorio", "script-output", "screenshots").toFile()); + jfc.setDialogTitle("Select dir of screenshots to stitch"); + jfc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + if (jfc.showOpenDialog(frame) != JFileChooser.APPROVE_OPTION) { + cleanup(); + return; + } + + try { + for (File split : jfc.getSelectedFile().listFiles()) { + String[] name = split.getName().split("_"); + if (!shots.containsKey(name[0])) + shots.put(name[0], new Screenshot()); + shots.get(name[0]).add(new Split(split, Byte.parseByte(name[1].substring(1)), + Byte.parseByte(name[2].substring(1, name[2].lastIndexOf('.'))))); + } + } catch (Exception e) { + JOptionPane.showMessageDialog(frame, "Something went wrong when loading screenshot files(" + e + + "). Make sure you picked the right directory."); + cleanup(); + return; + } + + bar.setMaximum(shots.size() + 1); + shotIterator = shots.values().iterator(); + + File output = new File("Stitched"); + output.mkdir(); + + for (byte i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + Screenshot shot; + while ((shot = next()) != null) { + try { + Split split = shot.getSplit(0); + BufferedImage s0 = ImageIO.read(split.file()); + int width = s0.getWidth(), height = s0.getHeight(); + + BufferedImage bi = new BufferedImage(width * shot.lengthX(), height * shot.lengthY(), + BufferedImage.TYPE_3BYTE_BGR); + Graphics g = bi.createGraphics(); + g.drawImage(s0, width * split.x(), height * split.y(), null); + + for (byte ind = 1; ind < shot.numSplits() && run; ind++) { + split = shot.getSplit(ind); + g.drawImage(ImageIO.read(split.file()), width * split.x(), height * split.y(), null); + } + + String fName = split.file().getName(); + ImageIO.write(bi, "jpg", new File(output, fName.substring(0, fName.indexOf('_')) + ".jpg")); + incProg(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + threads[i].start(); + } + + for (Thread t : threads) + try { + t.join(); + } catch (InterruptedException e) { + JOptionPane.showMessageDialog(frame, "An exception has occurred: " + e); + } + + if (run) + JOptionPane.showMessageDialog(frame, "Done."); + cleanup(); + } + + /** + * Flags the threads to stop and disposes frame. + */ + private void cleanup() { + run = false; + SwingUtilities.invokeLater(() -> frame.dispose()); + } + + /** + * Retrieves the next {@link Screenshot} to stitch. + * + * @return The next Screenshot or null if there are no more to stitch. + */ + private synchronized Screenshot next() { + return shotIterator.hasNext() ? shotIterator.next() : null; + } + + /** + * Increments the progress bar. + */ + private synchronized void incProg() { + SwingUtilities.invokeLater(() -> bar.setValue(bar.getValue() + 1)); + } + + @Override + public final void windowClosing(WindowEvent e) { + if (JOptionPane.showConfirmDialog(frame, "Are you sure you want to cancel stitching?", "Confirm exit", + JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) + cleanup(); + } + + /** + * Constructs and starts a new {@link Stitcher} instance. + * + * @param args Ignored. + */ + public static void main(String[] args) { + new Stitcher().start(); + } +} \ No newline at end of file