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