import java.util.Arrays;\r
import java.util.Collections;\r
import java.util.Comparator;\r
-import java.util.Iterator;\r
+import java.util.HashMap;\r
+import java.util.LinkedHashMap;\r
import java.util.regex.Matcher;\r
import java.util.regex.Pattern;\r
\r
import javax.imageio.ImageIO;\r
\r
-import com.badlogic.gdx.scenes.scene2d.actions.RotateBy;\r
+import com.badlogic.gdx.graphics.Pixmap.Format;\r
+import com.badlogic.gdx.graphics.Texture.TextureFilter;\r
import com.badlogic.gdx.utils.MathUtils;\r
\r
public class SpriteSheetPacker {\r
static Pattern numberedImagePattern = Pattern.compile(".*?(\\d+)");\r
\r
- private ArrayList<Image> images = new ArrayList();\r
+ ArrayList<Image> images = new ArrayList();\r
FileWriter writer;\r
final File inputDir;\r
- private int uncompressedSize, compressedSize;\r
- final Direction direction;\r
+ int uncompressedSize, compressedSize;\r
int xPadding, yPadding;\r
- private final Filter filter;\r
-\r
- // User configurable settings:\r
- private int alphaThreshold = 11;\r
- private boolean pot = true;\r
- private int padding = 0;\r
- private boolean debug = false;\r
- private boolean rotate = true;\r
- private int maxWidth = 1024;\r
- private int maxHeight = 1024;\r
-\r
- public SpriteSheetPacker (File inputDir, Filter filter, Direction direction, File outputDir, File packFile) throws IOException {\r
+ final Filter filter;\r
+ int minWidth, minHeight;\r
+ int maxWidth, maxHeight;\r
+ final Settings settings;\r
+\r
+ public SpriteSheetPacker (Settings settings, File inputDir, Filter filter, File outputDir, File packFile) throws IOException {\r
+ this.settings = settings;\r
this.inputDir = inputDir;\r
this.filter = filter;\r
- this.direction = direction;\r
\r
- ArrayList<File> files = getFiles(inputDir, filter, direction);\r
- if (files == null) return;\r
+ minWidth = filter.width != -1 ? filter.width : settings.minWidth;\r
+ minHeight = filter.height != -1 ? filter.height : settings.minHeight;\r
+ maxWidth = filter.width != -1 ? filter.width : settings.maxWidth;\r
+ maxHeight = filter.height != -1 ? filter.height : settings.maxHeight;\r
+ xPadding = images.size() > 1 && filter.direction != Direction.x && filter.direction != Direction.xy ? settings.padding : 0;\r
+ yPadding = images.size() > 1 && filter.direction != Direction.y && filter.direction != Direction.xy ? settings.padding : 0;\r
\r
// Collect and squeeze images.\r
+ File[] files = inputDir.listFiles(filter);\r
+ if (files == null) return;\r
for (File file : files) {\r
if (file.isDirectory()) continue;\r
Image image = squeeze(file);\r
}\r
if (images.isEmpty()) return;\r
\r
- // Print image names.\r
System.out.println(inputDir);\r
- if (filter != rgba8888) System.out.println("Format: " + filter.name);\r
- if (direction != null) System.out.println("Direction: " + direction);\r
-\r
- xPadding = images.size() > 1 && direction != Direction.x && direction != Direction.xy ? padding : 0;\r
- yPadding = images.size() > 1 && direction != Direction.y && direction != Direction.xy ? padding : 0;\r
+ if (filter.format != null)\r
+ System.out.println("Format: " + filter.format);\r
+ else\r
+ System.out.println("Format: " + settings.defaultFormat + " (default)");\r
+ if (filter.minFilter != null && filter.magFilter != null)\r
+ System.out.println("Filter: " + filter.minFilter + ", " + filter.magFilter);\r
+ else\r
+ System.out.println("Filter: " + settings.defaultFilterMin + ", " + settings.defaultFilterMag + " (default)");\r
+ if (filter.direction != Direction.none) System.out.println("Repeat: " + filter.direction);\r
\r
outputDir.mkdirs();\r
String prefix = inputDir.getName();\r
outputFile = new File(outputDir, prefix + ++imageNumber + ".png");\r
\r
writer.write("\n" + prefix + imageNumber + ".png\n");\r
- writer.write("repeat: " + direction + "\n");\r
- writer.write("filter: Linear,Linear\n"); // BOZO\r
+ Format format;\r
+ if (filter.format != null) {\r
+ writer.write("format: " + filter.format + "\n");\r
+ format = filter.format;\r
+ } else {\r
+ writer.write("format: " + settings.defaultFormat + "\n");\r
+ format = settings.defaultFormat;\r
+ }\r
+ if (filter.minFilter == null || filter.magFilter == null)\r
+ writer.write("filter: " + settings.defaultFilterMin + "," + settings.defaultFilterMag + "\n");\r
+ else\r
+ writer.write("filter: " + filter.minFilter + "," + filter.magFilter + "\n");\r
+ writer.write("repeat: " + filter.direction + "\n");\r
\r
// Try reasonably hard to pack images into the smallest POT size.\r
Comparator bestComparator = null;\r
int bestWidth = 99999, bestHeight = 99999;\r
int secondBestWidth = 99999, secondBestHeight = 99999;\r
int bestUsedPixels = 0;\r
- int width = 64, height = 64;\r
+ int width = minWidth, height = minHeight;\r
int grownPixels = 0, grownPixels2 = 0;\r
int i = 0, ii = 0;\r
while (true) {\r
}\r
}\r
if (bestComparator != null) break;\r
- if (pot) {\r
+ if (settings.pot) {\r
// 64,64 then 64,128 then 128,64 then 128,128 then 128,256 etc.\r
if (i % 3 == 0) {\r
width *= 2;\r
}\r
width = bestWidth;\r
height = bestHeight;\r
- if (pot) {\r
+ if (settings.pot) {\r
width = MathUtils.nextPowerOfTwo(width);\r
height = MathUtils.nextPowerOfTwo(height);\r
}\r
\r
- BufferedImage canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);\r
+ int type;\r
+ switch (format) {\r
+ case RGBA8888:\r
+ case RGBA4444:\r
+ type = BufferedImage.TYPE_INT_ARGB;\r
+ break;\r
+ case RGB565:\r
+ type = BufferedImage.TYPE_INT_RGB;\r
+ break;\r
+ case Alpha:\r
+ type = BufferedImage.TYPE_BYTE_GRAY;\r
+ break;\r
+ default:\r
+ throw new RuntimeException();\r
+ }\r
+ BufferedImage canvas = new BufferedImage(width, height, type);\r
insert(canvas, images, bestWidth, bestHeight);\r
System.out.println("Writing " + canvas.getWidth() + "x" + canvas.getHeight() + ": " + outputFile);\r
ImageIO.write(canvas, "png", outputFile);\r
}\r
\r
private int insert (BufferedImage canvas, ArrayList<Image> images, int width, int height) throws IOException {\r
- if (debug && canvas != null) {\r
+ if (settings.debug && canvas != null) {\r
Graphics g = canvas.getGraphics();\r
g.setColor(Color.green);\r
g.drawRect(0, 0, width - 1, height - 1);\r
}\r
// Pretend image is larger so padding on right and bottom edges is ignored.\r
- if (direction != Direction.x && direction != Direction.xy) width += xPadding;\r
- if (direction != Direction.y && direction != Direction.xy) height += yPadding;\r
+ if (filter.direction != Direction.x && filter.direction != Direction.xy) width += xPadding;\r
+ if (filter.direction != Direction.y && filter.direction != Direction.xy) height += yPadding;\r
Node root = new Node(0, 0, width, height);\r
int usedPixels = 0;\r
for (int i = images.size() - 1; i >= 0; i--) {\r
Image image = images.get(i);\r
Node node = root.insert(image, canvas, false);\r
if (node == null) {\r
- if (rotate) node = root.insert(image, canvas, true);\r
+ if (settings.rotate) node = root.insert(image, canvas, true);\r
if (node == null) continue;\r
}\r
usedPixels += image.getWidth() * image.getHeight();\r
g.rotate(90 * MathUtils.degreesToRadians);\r
g.translate(-node.left, -node.top);\r
}\r
- if (debug) {\r
+ if (settings.debug) {\r
g.setColor(Color.magenta);\r
int imageWidth = image.getWidth();\r
int imageHeight = image.getHeight();\r
private Image squeeze (File file) throws IOException {\r
BufferedImage source = ImageIO.read(file);\r
if (source == null) return null;\r
+ if (!filter.accept(source)) return null;\r
uncompressedSize += source.getWidth() * source.getHeight();\r
WritableRaster alphaRaster = source.getAlphaRaster();\r
if (alphaRaster == null) return new Image(file, source, 0, 0, source.getWidth(), source.getHeight());\r
final byte[] a = new byte[1];\r
int top = 0;\r
- outer:\r
- for (int y = 0; y < source.getHeight(); y++) {\r
- for (int x = 0; x < source.getWidth(); x++) {\r
- alphaRaster.getDataElements(x, y, a);\r
- int alpha = a[0];\r
- if (alpha < 0) alpha += 256;\r
- if (alpha > alphaThreshold) break outer;\r
+ int bottom = source.getHeight();\r
+ if (filter.direction != Direction.y && filter.direction != Direction.xy) {\r
+ outer:\r
+ for (int y = 0; y < source.getHeight(); y++) {\r
+ for (int x = 0; x < source.getWidth(); x++) {\r
+ alphaRaster.getDataElements(x, y, a);\r
+ int alpha = a[0];\r
+ if (alpha < 0) alpha += 256;\r
+ if (alpha > settings.alphaThreshold) break outer;\r
+ }\r
+ top++;\r
}\r
- top++;\r
- }\r
- int bottom = source.getHeight() - 1;\r
- outer:\r
- for (int y = source.getHeight(); --y >= top;) {\r
- for (int x = 0; x < source.getWidth(); x++) {\r
- alphaRaster.getDataElements(x, y, a);\r
- int alpha = a[0];\r
- if (alpha < 0) alpha += 256;\r
- if (alpha > alphaThreshold) break outer;\r
+ outer:\r
+ for (int y = source.getHeight(); --y >= top;) {\r
+ for (int x = 0; x < source.getWidth(); x++) {\r
+ alphaRaster.getDataElements(x, y, a);\r
+ int alpha = a[0];\r
+ if (alpha < 0) alpha += 256;\r
+ if (alpha > settings.alphaThreshold) break outer;\r
+ }\r
+ bottom--;\r
}\r
- bottom--;\r
}\r
int left = 0;\r
- outer:\r
- for (int x = 0; x < source.getWidth(); x++) {\r
- for (int y = top; y <= bottom; y++) {\r
- alphaRaster.getDataElements(x, y, a);\r
- int alpha = a[0];\r
- if (alpha < 0) alpha += 256;\r
- if (alpha > alphaThreshold) break outer;\r
+ int right = source.getWidth();\r
+ if (filter.direction != Direction.x && filter.direction != Direction.xy) {\r
+ outer:\r
+ for (int x = 0; x < source.getWidth(); x++) {\r
+ for (int y = top; y <= bottom; y++) {\r
+ alphaRaster.getDataElements(x, y, a);\r
+ int alpha = a[0];\r
+ if (alpha < 0) alpha += 256;\r
+ if (alpha > settings.alphaThreshold) break outer;\r
+ }\r
+ left++;\r
}\r
- left++;\r
- }\r
- int right = source.getWidth() - 1;\r
- outer:\r
- for (int x = source.getWidth(); --x >= left;) {\r
- for (int y = top; y <= bottom; y++) {\r
- alphaRaster.getDataElements(x, y, a);\r
- int alpha = a[0];\r
- if (alpha < 0) alpha += 256;\r
- if (alpha > alphaThreshold) break outer;\r
+ outer:\r
+ for (int x = source.getWidth(); --x >= left;) {\r
+ for (int y = top; y <= bottom; y++) {\r
+ alphaRaster.getDataElements(x, y, a);\r
+ int alpha = a[0];\r
+ if (alpha < 0) alpha += 256;\r
+ if (alpha > settings.alphaThreshold) break outer;\r
+ }\r
+ right--;\r
}\r
- right--;\r
}\r
int newWidth = right - left;\r
int newHeight = bottom - top;\r
this.height = height;\r
}\r
\r
+ /**\r
+ * Returns true if the image was inserted. If canvas != null, an entry is written to the pack file.\r
+ */\r
public Node insert (Image image, BufferedImage canvas, boolean rotate) throws IOException {\r
if (this.image != null) return null;\r
if (child1 != null) {\r
if (imageName.startsWith("/") || imageName.startsWith("\\")) imageName = imageName.substring(1);\r
int dotIndex = imageName.lastIndexOf('.');\r
if (dotIndex != -1) imageName = imageName.substring(0, dotIndex);\r
- if (imageName.endsWith("_4444")) imageName = imageName.substring(0, imageName.length() - 5);\r
- if (imageName.endsWith("_565")) imageName = imageName.substring(0, imageName.length() - 4);\r
- if (imageName.endsWith("_a")) imageName = imageName.substring(0, imageName.length() - 2);\r
- if (imageName.endsWith("_pre")) imageName = imageName.substring(0, imageName.length() - 2);\r
+ imageName = imageName.replace("_" + filter.format, "");\r
+ imageName = imageName.replace("_" + filter.direction, "");\r
+ imageName = imageName.replace("_" + filterToAbbrev.get(filter.minFilter) + "," + filterToAbbrev.get(filter.magFilter),\r
+ "");\r
\r
writer.write(imageName.replace("\\", "/") + "\n");\r
writer.write(" rotate: " + image.rotate + "\n");\r
\r
Matcher matcher = numberedImagePattern.matcher(imageName);\r
if (matcher.matches())\r
- writer.write("offset: " + Integer.parseInt(matcher.group(1)) + "\n");\r
+ writer.write(" index: " + Integer.parseInt(matcher.group(1)) + "\n");\r
else\r
- writer.write("offset: 0\n");\r
+ writer.write(" index: 0\n");\r
}\r
}\r
\r
});\r
}\r
\r
- static private Filter rgba8888 = new Filter("RGBA8888") {\r
- public boolean accept (File dir, String name) {\r
- return !name.endsWith("_4444") && !name.endsWith("_565") && !name.endsWith("_a");\r
- }\r
- };\r
- static private Filter rgba4444 = new Filter("RGBA4444") {\r
- public boolean accept (File dir, String name) {\r
- return name.endsWith("_4444");\r
- }\r
- };\r
- static private Filter rgb565 = new Filter("RGB565") {\r
- public boolean accept (File dir, String name) {\r
- return name.endsWith("_565");\r
- }\r
- };\r
- static private Filter alpha = new Filter("Alpha") {\r
- public boolean accept (File dir, String name) {\r
- return name.endsWith("_a");\r
- }\r
- };\r
-\r
- static abstract private class Filter implements FilenameFilter {\r
- String name;\r
-\r
- public Filter (String name) {\r
- this.name = name;\r
+ static private class Filter implements FilenameFilter {\r
+ Direction direction;\r
+ Format format;\r
+ TextureFilter minFilter;\r
+ TextureFilter magFilter;\r
+ int width = -1;\r
+ int height = -1;\r
+ Settings settings;\r
+\r
+ public Filter (Settings settings, Direction direction, Format format, int width, int height, TextureFilter minFilter,\r
+ TextureFilter magFilter) {\r
+ this.settings = settings;\r
+ this.direction = direction;\r
+ this.format = format;\r
+ this.width = width;\r
+ this.height = height;\r
+ this.minFilter = minFilter;\r
+ this.magFilter = magFilter;\r
}\r
- }\r
-\r
- static private enum Direction {\r
- x, y, xy, none\r
- }\r
\r
- static private ArrayList<File> getFiles (File inputDir, Filter filter, Direction direction) {\r
- ArrayList<File> files = new ArrayList();\r
- files.addAll(Arrays.asList(inputDir.listFiles(filter)));\r
- for (Iterator<File> iter = files.iterator(); iter.hasNext();) {\r
- File file = iter.next();\r
- String name = file.getName();\r
+ public boolean accept (File dir, String name) {\r
switch (direction) {\r
case none:\r
- if (name.contains("_x") || name.contains("_y")) iter.remove();\r
+ if (name.contains("_x") || name.contains("_y")) return false;\r
break;\r
case x:\r
- if (!name.contains("_x") || name.contains("_xy")) iter.remove();\r
+ if (!name.contains("_x") || name.contains("_xy")) return false;\r
break;\r
case y:\r
- if (!name.contains("_y")) iter.remove();\r
+ if (!name.contains("_y") || name.contains("_xy")) return false;\r
break;\r
case xy:\r
- if (!name.contains("_xy")) iter.remove();\r
+ if (!name.contains("_xy")) return false;\r
break;\r
}\r
+\r
+ if (format != null) {\r
+ if (!name.contains("_" + formatToAbbrev.get(format))) return false;\r
+ } else {\r
+ // Return if name has a format.\r
+ for (String f : formatToAbbrev.values())\r
+ if (name.contains("_" + f)) return false;\r
+ }\r
+\r
+ if (minFilter != null && magFilter != null) {\r
+ if (!name.contains("_" + filterToAbbrev.get(minFilter) + "," + filterToAbbrev.get(magFilter) + ".")\r
+ && !name.contains("_" + filterToAbbrev.get(minFilter) + "," + filterToAbbrev.get(magFilter) + "_")) return false;\r
+ } else {\r
+ // Return if the name has a filter.\r
+ for (String f : filterToAbbrev.values()) {\r
+ String tag = "_" + f + ",";\r
+ int tagIndex = name.indexOf(tag);\r
+ if (tagIndex != -1) {\r
+ String rest = name.substring(tagIndex + tag.length());\r
+ for (String f2 : filterToAbbrev.values())\r
+ if (rest.startsWith(f2 + ".") || rest.startsWith(f2 + "_")) return false;\r
+ }\r
+ }\r
+ }\r
+\r
+ return true;\r
}\r
- return files;\r
+\r
+ public boolean accept (BufferedImage image) {\r
+ if (width != -1 && image.getWidth() != width) return false;\r
+ if (height != -1 && image.getHeight() != height) return false;\r
+ return true;\r
+ }\r
+ }\r
+\r
+ static private enum Direction {\r
+ x, y, xy, none\r
+ }\r
+\r
+ static final HashMap<TextureFilter, String> filterToAbbrev = new HashMap();\r
+ static {\r
+ filterToAbbrev.put(TextureFilter.Linear, "l");\r
+ filterToAbbrev.put(TextureFilter.Nearest, "n");\r
+ filterToAbbrev.put(TextureFilter.MipMap, "m");\r
+ filterToAbbrev.put(TextureFilter.MipMapLinearLinear, "mll");\r
+ filterToAbbrev.put(TextureFilter.MipMapLinearNearest, "mln");\r
+ filterToAbbrev.put(TextureFilter.MipMapNearestLinear, "mnl");\r
+ filterToAbbrev.put(TextureFilter.MipMapNearestNearest, "mnn");\r
}\r
\r
- static private void process (File inputDir, File outputDir, File packFile) throws Exception {\r
+ static final HashMap<Format, String> formatToAbbrev = new HashMap();\r
+ static {\r
+ formatToAbbrev.put(Format.RGBA8888, "8888");\r
+ formatToAbbrev.put(Format.RGBA4444, "4444");\r
+ formatToAbbrev.put(Format.RGB565, "565");\r
+ formatToAbbrev.put(Format.Alpha, "a");\r
+ }\r
+\r
+ static class Settings {\r
+ public Format defaultFormat = Format.RGBA8888;\r
+ public TextureFilter defaultFilterMin = TextureFilter.Linear;\r
+ public TextureFilter defaultFilterMag = TextureFilter.Linear;\r
+ public int alphaThreshold = 9;\r
+ public boolean pot = true;\r
+ public int padding = 0;\r
+ public boolean debug = false;\r
+ public boolean rotate = true;\r
+ public int minWidth = 64;\r
+ public int minHeight = 64;\r
+ public int maxWidth = 1024;\r
+ public int maxHeight = 1024;\r
+ }\r
+\r
+ static private void process (Settings settings, File inputDir, File outputDir, File packFile) throws Exception {\r
+ // Clean existing page images.\r
if (outputDir.exists()) {\r
String prefix = inputDir.getName();\r
for (File file : outputDir.listFiles())\r
if (file.getName().startsWith(prefix)) file.delete();\r
}\r
\r
- Direction[] directions = Direction.values();\r
- for (int i = 0; i < directions.length; i++) {\r
- Direction direction = directions[i];\r
- new SpriteSheetPacker(inputDir, rgba8888, direction, outputDir, packFile);\r
- new SpriteSheetPacker(inputDir, rgba4444, direction, outputDir, packFile);\r
- new SpriteSheetPacker(inputDir, rgb565, direction, outputDir, packFile);\r
- new SpriteSheetPacker(inputDir, alpha, direction, outputDir, packFile);\r
+ // Just check all combinations, because we are extremely lazy.\r
+ ArrayList<TextureFilter> filters = new ArrayList();\r
+ filters.add(null);\r
+ filters.addAll(Arrays.asList(TextureFilter.values()));\r
+ ArrayList<Format> formats = new ArrayList();\r
+ formats.add(null);\r
+ formats.addAll(Arrays.asList(Format.values()));\r
+ for (int i = 0, n = formats.size(); i < n; i++) {\r
+ Format format = formats.get(i);\r
+ for (int ii = 0, nn = filters.size(); ii < nn; ii++) {\r
+ TextureFilter min = filters.get(ii);\r
+ for (int iii = ii; iii < nn; iii++) {\r
+ TextureFilter mag = filters.get(iii);\r
+ if ((min == null && mag != null) || (min != null && mag == null)) continue;\r
+\r
+ Filter filter = new Filter(settings, Direction.none, format, -1, -1, min, mag);\r
+ new SpriteSheetPacker(settings, inputDir, filter, outputDir, packFile);\r
+\r
+ for (int width = settings.minWidth; width <= settings.maxWidth; width <<= 1) {\r
+ filter = new Filter(settings, Direction.x, format, width, -1, min, mag);\r
+ new SpriteSheetPacker(settings, inputDir, filter, outputDir, packFile);\r
+ }\r
+\r
+ for (int height = settings.minHeight; height <= settings.maxHeight; height <<= 1) {\r
+ filter = new Filter(settings, Direction.y, format, -1, height, min, mag);\r
+ new SpriteSheetPacker(settings, inputDir, filter, outputDir, packFile);\r
+ }\r
+\r
+ for (int width = settings.minWidth; width <= settings.maxWidth; width <<= 1) {\r
+ for (int height = settings.minHeight; height <= settings.maxHeight; height <<= 1) {\r
+ filter = new Filter(settings, Direction.xy, format, width, height, min, mag);\r
+ new SpriteSheetPacker(settings, inputDir, filter, outputDir, packFile);\r
+ }\r
+ }\r
+ }\r
+ }\r
}\r
+\r
+ // Process subdirectories.\r
File[] files = inputDir.listFiles();\r
if (files == null) return;\r
for (File file : files)\r
- if (file.isDirectory()) process(file, new File(outputDir, file.getName()), packFile);\r
+ if (file.isDirectory()) process(settings, file, new File(outputDir, file.getName()), packFile);\r
}\r
\r
- static public void process (String inputDir, String outputDir) throws Exception {\r
- File input = new File(inputDir);\r
- if (!input.isDirectory()) {\r
- System.out.println("Not a directory: " + input);\r
+ static public void process (Settings settings, String input, String output) throws Exception {\r
+ File inputDir = new File(input);\r
+ File outputDir = new File(output);\r
+\r
+ if (!inputDir.isDirectory()) {\r
+ System.out.println("Not a directory: " + inputDir);\r
return;\r
}\r
+\r
+ // Clean pack file.\r
File packFile = new File(outputDir, "pack");\r
packFile.delete();\r
- process(new File(inputDir), new File(outputDir), packFile);\r
+\r
+ process(settings, inputDir, outputDir, packFile);\r
}\r
\r
public static void main (String[] args) throws Exception {\r
+ String input, output;\r
if (args.length != 2) {\r
System.out.println("Usage: INPUTDIR OUTPUTDIR");\r
return;\r
}\r
- process(args[0], args[1]);\r
- // process("c:/temp/pack-in", "c:/temp/pack-out");\r
+ input = args[0];\r
+ output = args[1];\r
+ // input = "c:/temp/pack-in";\r
+ // output = "c:/temp/pack-out";\r
+ process(new Settings(), input, output);\r
}\r
}\r