/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.badlogic.gdx.tools.texturepacker;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Arrays;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData.Region;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.Json;
/** @author Nathan Sweet */
public class TexturePacker {
private final Settings settings;
private final Packer packer;
private final ImageProcessor imageProcessor;
private final Array<InputImage> inputImages = new Array();
private File rootDir;
/** @param rootDir Used to strip the root directory prefix from image file names, can be null. */
public TexturePacker (File rootDir, Settings settings) {
this.rootDir = rootDir;
this.settings = settings;
if (settings.pot) {
if (settings.maxWidth != MathUtils.nextPowerOfTwo(settings.maxWidth))
throw new RuntimeException("If pot is true, maxWidth must be a power of two: " + settings.maxWidth);
if (settings.maxHeight != MathUtils.nextPowerOfTwo(settings.maxHeight))
throw new RuntimeException("If pot is true, maxHeight must be a power of two: " + settings.maxHeight);
}
if (settings.grid)
packer = new GridPacker(settings);
else
packer = new MaxRectsPacker(settings);
imageProcessor = new ImageProcessor(rootDir, settings);
}
public TexturePacker (Settings settings) {
this(null, settings);
}
public void addImage (File file) {
InputImage inputImage = new InputImage();
inputImage.file = file;
inputImages.add(inputImage);
}
public void addImage (BufferedImage image, String name) {
InputImage inputImage = new InputImage();
inputImage.image = image;
inputImage.name = name;
inputImages.add(inputImage);
}
public void pack (File outputDir, String packFileName) {
if (packFileName.endsWith(settings.atlasExtension))
packFileName = packFileName.substring(0, packFileName.length() - settings.atlasExtension.length());
outputDir.mkdirs();
for (int i = 0, n = settings.scale.length; i < n; i++) {
imageProcessor.setScale(settings.scale[i]);
for (InputImage inputImage : inputImages) {
if (inputImage.file != null)
imageProcessor.addImage(inputImage.file);
else
imageProcessor.addImage(inputImage.image, inputImage.name);
}
Array<Page> pages = packer.pack(imageProcessor.getImages());
String scaledPackFileName = settings.getScaledPackFileName(packFileName, i);
writeImages(outputDir, scaledPackFileName, pages);
try {
writePackFile(outputDir, scaledPackFileName, pages);
} catch (IOException ex) {
throw new RuntimeException("Error writing pack file.", ex);
}
imageProcessor.clear();
}
}
private void writeImages (File outputDir, String scaledPackFileName, Array<Page> pages) {
File packFileNoExt = new File(outputDir, scaledPackFileName);
File packDir = packFileNoExt.getParentFile();
String imageName = packFileNoExt.getName();
int fileIndex = 0;
for (Page page : pages) {
int width = page.width, height = page.height;
int paddingX = settings.paddingX;
int paddingY = settings.paddingY;
if (settings.duplicatePadding) {
paddingX /= 2;
paddingY /= 2;
}
width -= settings.paddingX;
height -= settings.paddingY;
if (settings.edgePadding) {
page.x = paddingX;
page.y = paddingY;
width += paddingX * 2;
height += paddingY * 2;
}
if (settings.pot) {
width = MathUtils.nextPowerOfTwo(width);
height = MathUtils.nextPowerOfTwo(height);
}
width = Math.max(settings.minWidth, width);
height = Math.max(settings.minHeight, height);
page.imageWidth = width;
page.imageHeight = height;
File outputFile;
while (true) {
outputFile = new File(packDir, imageName + (fileIndex++ == 0 ? "" : fileIndex) + "." + settings.outputFormat);
if (!outputFile.exists()) break;
}
new FileHandle(outputFile).parent().mkdirs();
page.imageName = outputFile.getName();
BufferedImage canvas = new BufferedImage(width, height, getBufferedImageType(settings.format));
Graphics2D g = (Graphics2D)canvas.getGraphics();
if (!settings.silent) System.out.println("Writing " + canvas.getWidth() + "x" + canvas.getHeight() + ": " + outputFile);
for (Rect rect : page.outputRects) {
BufferedImage image = rect.getImage(imageProcessor);
int iw = image.getWidth();
int ih = image.getHeight();
int rectX = page.x + rect.x, rectY = page.y + page.height - rect.y - rect.height;
if (settings.duplicatePadding) {
int amountX = settings.paddingX / 2;
int amountY = settings.paddingY / 2;
if (rect.rotated) {
// Copy corner pixels to fill corners of the padding.
for (int i = 1; i <= amountX; i++) {
for (int j = 1; j <= amountY; j++) {
plot(canvas, rectX - j, rectY + iw - 1 + i, image.getRGB(0, 0));
plot(canvas, rectX + ih - 1 + j, rectY + iw - 1 + i, image.getRGB(0, ih - 1));
plot(canvas, rectX - j, rectY - i, image.getRGB(iw - 1, 0));
plot(canvas, rectX + ih - 1 + j, rectY - i, image.getRGB(iw - 1, ih - 1));
}
}
// Copy edge pixels into padding.
for (int i = 1; i <= amountY; i++) {
for (int j = 0; j < iw; j++) {
plot(canvas, rectX - i, rectY + iw - 1 - j, image.getRGB(j, 0));
plot(canvas, rectX + ih - 1 + i, rectY + iw - 1 - j, image.getRGB(j, ih - 1));
}
}
for (int i = 1; i <= amountX; i++) {
for (int j = 0; j < ih; j++) {
plot(canvas, rectX + j, rectY - i, image.getRGB(iw - 1, j));
plot(canvas, rectX + j, rectY + iw - 1 + i, image.getRGB(0, j));
}
}
} else {
// Copy corner pixels to fill corners of the padding.
for (int i = 1; i <= amountX; i++) {
for (int j = 1; j <= amountY; j++) {
plot(canvas, rectX - i, rectY - j, image.getRGB(0, 0));
plot(canvas, rectX - i, rectY + ih - 1 + j, image.getRGB(0, ih - 1));
plot(canvas, rectX + iw - 1 + i, rectY - j, image.getRGB(iw - 1, 0));
plot(canvas, rectX + iw - 1 + i, rectY + ih - 1 + j, image.getRGB(iw - 1, ih - 1));
}
}
// Copy edge pixels into padding.
for (int i = 1; i <= amountY; i++) {
copy(image, 0, 0, iw, 1, canvas, rectX, rectY - i, rect.rotated);
copy(image, 0, ih - 1, iw, 1, canvas, rectX, rectY + ih - 1 + i, rect.rotated);
}
for (int i = 1; i <= amountX; i++) {
copy(image, 0, 0, 1, ih, canvas, rectX - i, rectY, rect.rotated);
copy(image, iw - 1, 0, 1, ih, canvas, rectX + iw - 1 + i, rectY, rect.rotated);
}
}
}
copy(image, 0, 0, iw, ih, canvas, rectX, rectY, rect.rotated);
if (settings.debug) {
g.setColor(Color.magenta);
g.drawRect(rectX, rectY, rect.width - settings.paddingX - 1, rect.height - settings.paddingY - 1);
}
}
if (settings.bleed && !settings.premultiplyAlpha
&& !(settings.outputFormat.equalsIgnoreCase("jpg") || settings.outputFormat.equalsIgnoreCase("jpeg"))) {
canvas = new ColorBleedEffect().processImage(canvas, settings.bleedIterations);
g = (Graphics2D)canvas.getGraphics();
}
if (settings.debug) {
g.setColor(Color.magenta);
g.drawRect(0, 0, width - 1, height - 1);
}
ImageOutputStream ios = null;
try {
if (settings.outputFormat.equalsIgnoreCase("jpg") || settings.outputFormat.equalsIgnoreCase("jpeg")) {
BufferedImage newImage = new BufferedImage(canvas.getWidth(), canvas.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
newImage.getGraphics().drawImage(canvas, 0, 0, null);
canvas = newImage;
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
ImageWriter writer = writers.next();
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(settings.jpegQuality);
ios = ImageIO.createImageOutputStream(outputFile);
writer.setOutput(ios);
writer.write(null, new IIOImage(canvas, null, null), param);
} else {
if (settings.premultiplyAlpha) canvas.getColorModel().coerceData(canvas.getRaster(), true);
ImageIO.write(canvas, "png", outputFile);
}
} catch (IOException ex) {
throw new RuntimeException("Error writing file: " + outputFile, ex);
} finally {
if (ios != null) {
try {
ios.close();
} catch (Exception ignored) {
}
}
}
}
}
static private void plot (BufferedImage dst, int x, int y, int argb) {
if (0 <= x && x < dst.getWidth() && 0 <= y && y < dst.getHeight()) dst.setRGB(x, y, argb);
}
static private void copy (BufferedImage src, int x, int y, int w, int h, BufferedImage dst, int dx, int dy, boolean rotated) {
if (rotated) {
for (int i = 0; i < w; i++)
for (int j = 0; j < h; j++)
plot(dst, dx + j, dy + w - i - 1, src.getRGB(x + i, y + j));
} else {
for (int i = 0; i < w; i++)
for (int j = 0; j < h; j++)
plot(dst, dx + i, dy + j, src.getRGB(x + i, y + j));
}
}
private void writePackFile (File outputDir, String scaledPackFileName, Array<Page> pages) throws IOException {
File packFile = new File(outputDir, scaledPackFileName + settings.atlasExtension);
File packDir = packFile.getParentFile();
packDir.mkdirs();
if (packFile.exists()) {
// Make sure there aren't duplicate names.
TextureAtlasData textureAtlasData = new TextureAtlasData(new FileHandle(packFile), new FileHandle(packFile), false);
for (Page page : pages) {
for (Rect rect : page.outputRects) {
String rectName = Rect.getAtlasName(rect.name, settings.flattenPaths);
for (Region region : textureAtlasData.getRegions()) {
if (region.name.equals(rectName)) {
throw new GdxRuntimeException(
"A region with the name \"" + rectName + "\" has already been packed: " + rect.name);
}
}
}
}
}
Writer writer = new OutputStreamWriter(new FileOutputStream(packFile, true), "UTF-8");
for (Page page : pages) {
writer.write("\n" + page.imageName + "\n");
writer.write("size: " + page.imageWidth + "," + page.imageHeight + "\n");
writer.write("format: " + settings.format + "\n");
writer.write("filter: " + settings.filterMin + "," + settings.filterMag + "\n");
writer.write("repeat: " + getRepeatValue() + "\n");
page.outputRects.sort();
for (Rect rect : page.outputRects) {
writeRect(writer, page, rect, rect.name);
Array<Alias> aliases = new Array(rect.aliases.toArray());
aliases.sort();
for (Alias alias : aliases) {
Rect aliasRect = new Rect();
aliasRect.set(rect);
alias.apply(aliasRect);
writeRect(writer, page, aliasRect, alias.name);
}
}
}
writer.close();
}
private void writeRect (Writer writer, Page page, Rect rect, String name) throws IOException {
writer.write(Rect.getAtlasName(name, settings.flattenPaths) + "\n");
writer.write(" rotate: " + rect.rotated + "\n");
writer.write(" xy: " + (page.x + rect.x) + ", " + (page.y + page.height - rect.height - rect.y) + "\n");
writer.write(" size: " + rect.regionWidth + ", " + rect.regionHeight + "\n");
if (rect.splits != null) {
writer.write(" split: " //
+ rect.splits[0] + ", " + rect.splits[1] + ", " + rect.splits[2] + ", " + rect.splits[3] + "\n");
}
if (rect.pads != null) {
if (rect.splits == null) writer.write(" split: 0, 0, 0, 0\n");
writer.write(" pad: " + rect.pads[0] + ", " + rect.pads[1] + ", " + rect.pads[2] + ", " + rect.pads[3] + "\n");
}
writer.write(" orig: " + rect.originalWidth + ", " + rect.originalHeight + "\n");
writer.write(" offset: " + rect.offsetX + ", " + (rect.originalHeight - rect.regionHeight - rect.offsetY) + "\n");
writer.write(" index: " + rect.index + "\n");
}
private String getRepeatValue () {
if (settings.wrapX == TextureWrap.Repeat && settings.wrapY == TextureWrap.Repeat) return "xy";
if (settings.wrapX == TextureWrap.Repeat && settings.wrapY == TextureWrap.ClampToEdge) return "x";
if (settings.wrapX == TextureWrap.ClampToEdge && settings.wrapY == TextureWrap.Repeat) return "y";
return "none";
}
private int getBufferedImageType (Format format) {
switch (settings.format) {
case RGBA8888:
case RGBA4444:
return BufferedImage.TYPE_INT_ARGB;
case RGB565:
case RGB888:
return BufferedImage.TYPE_INT_RGB;
case Alpha:
return BufferedImage.TYPE_BYTE_GRAY;
default:
throw new RuntimeException("Unsupported format: " + settings.format);
}
}
/** @author Nathan Sweet */
static public class Page {
public String imageName;
public Array<Rect> outputRects, remainingRects;
public float occupancy;
public int x, y, width, height, imageWidth, imageHeight;
}
/** @author Regnarock
* @author Nathan Sweet */
static public class Alias implements Comparable<Alias> {
public String name;
public int index;
public int[] splits;
public int[] pads;
public int offsetX, offsetY, originalWidth, originalHeight;
public Alias (Rect rect) {
name = rect.name;
index = rect.index;
splits = rect.splits;
pads = rect.pads;
offsetX = rect.offsetX;
offsetY = rect.offsetY;
originalWidth = rect.originalWidth;
originalHeight = rect.originalHeight;
}
public void apply (Rect rect) {
rect.name = name;
rect.index = index;
rect.splits = splits;
rect.pads = pads;
rect.offsetX = offsetX;
rect.offsetY = offsetY;
rect.originalWidth = originalWidth;
rect.originalHeight = originalHeight;
}
public int compareTo (Alias o) {
return name.compareTo(o.name);
}
}
/** @author Nathan Sweet */
static public class Rect implements Comparable<Rect> {
public String name;
public int offsetX, offsetY, regionWidth, regionHeight, originalWidth, originalHeight;
public int x, y;
public int width, height; // Portion of page taken by this region, including padding.
public int index;
public boolean rotated;
public Set<Alias> aliases = new HashSet<Alias>();
public int[] splits;
public int[] pads;
public boolean canRotate = true;
private boolean isPatch;
private BufferedImage image;
private File file;
int score1, score2;
Rect (BufferedImage source, int left, int top, int newWidth, int newHeight, boolean isPatch) {
image = new BufferedImage(source.getColorModel(),
source.getRaster().createWritableChild(left, top, newWidth, newHeight, 0, 0, null),
source.getColorModel().isAlphaPremultiplied(), null);
offsetX = left;
offsetY = top;
regionWidth = newWidth;
regionHeight = newHeight;
originalWidth = source.getWidth();
originalHeight = source.getHeight();
width = newWidth;
height = newHeight;
this.isPatch = isPatch;
}
/** Clears the image for this rect, which will be loaded from the specified file by {@link #getImage(ImageProcessor)}. */
public void unloadImage (File file) {
this.file = file;
image = null;
}
public BufferedImage getImage (ImageProcessor imageProcessor) {
if (image != null) return image;
BufferedImage image;
try {
image = ImageIO.read(file);
} catch (IOException ex) {
throw new RuntimeException("Error reading image: " + file, ex);
}
if (image == null) throw new RuntimeException("Unable to read image: " + file);
String name = this.name;
if (isPatch) name += ".9";
return imageProcessor.processImage(image, name).getImage(null);
}
Rect () {
}
Rect (Rect rect) {
x = rect.x;
y = rect.y;
width = rect.width;
height = rect.height;
}
void set (Rect rect) {
name = rect.name;
image = rect.image;
offsetX = rect.offsetX;
offsetY = rect.offsetY;
regionWidth = rect.regionWidth;
regionHeight = rect.regionHeight;
originalWidth = rect.originalWidth;
originalHeight = rect.originalHeight;
x = rect.x;
y = rect.y;
width = rect.width;
height = rect.height;
index = rect.index;
rotated = rect.rotated;
aliases = rect.aliases;
splits = rect.splits;
pads = rect.pads;
canRotate = rect.canRotate;
score1 = rect.score1;
score2 = rect.score2;
file = rect.file;
isPatch = rect.isPatch;
}
public int compareTo (Rect o) {
return name.compareTo(o.name);
}
@Override
public boolean equals (Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Rect other = (Rect)obj;
if (name == null) {
if (other.name != null) return false;
} else if (!name.equals(other.name)) return false;
return true;
}
@Override
public String toString () {
return name + "[" + x + "," + y + " " + width + "x" + height + "]";
}
static public String getAtlasName (String name, boolean flattenPaths) {
return flattenPaths ? new FileHandle(name).name() : name;
}
}
/** @author Nathan Sweet */
static public class Settings {
public boolean pot = true;
public int paddingX = 2, paddingY = 2;
public boolean edgePadding = true;
public boolean duplicatePadding = false;
public boolean rotation;
public int minWidth = 16, minHeight = 16;
public int maxWidth = 1024, maxHeight = 1024;
public boolean square = false;
public boolean stripWhitespaceX, stripWhitespaceY;
public int alphaThreshold;
public TextureFilter filterMin = TextureFilter.Nearest, filterMag = TextureFilter.Nearest;
public TextureWrap wrapX = TextureWrap.ClampToEdge, wrapY = TextureWrap.ClampToEdge;
public Format format = Format.RGBA8888;
public boolean alias = true;
public String outputFormat = "png";
public float jpegQuality = 0.9f;
public boolean ignoreBlankImages = true;
public boolean fast;
public boolean debug;
public boolean silent;
public boolean combineSubdirectories;
public boolean ignore;
public boolean flattenPaths;
public boolean premultiplyAlpha;
public boolean useIndexes = true;
public boolean bleed = true;
public int bleedIterations = 2;
public boolean limitMemory = true;
public boolean grid;
public float[] scale = {1};
public String[] scaleSuffix = {""};
public String atlasExtension = ".atlas";
public Settings () {
}
/** @see #set(Settings) */
public Settings (Settings settings) {
set(settings);
}
/** Copies values from another instance to the current one */
public void set (Settings settings) {
fast = settings.fast;
rotation = settings.rotation;
pot = settings.pot;
minWidth = settings.minWidth;
minHeight = settings.minHeight;
maxWidth = settings.maxWidth;
maxHeight = settings.maxHeight;
paddingX = settings.paddingX;
paddingY = settings.paddingY;
edgePadding = settings.edgePadding;
duplicatePadding = settings.duplicatePadding;
alphaThreshold = settings.alphaThreshold;
ignoreBlankImages = settings.ignoreBlankImages;
stripWhitespaceX = settings.stripWhitespaceX;
stripWhitespaceY = settings.stripWhitespaceY;
alias = settings.alias;
format = settings.format;
jpegQuality = settings.jpegQuality;
outputFormat = settings.outputFormat;
filterMin = settings.filterMin;
filterMag = settings.filterMag;
wrapX = settings.wrapX;
wrapY = settings.wrapY;
debug = settings.debug;
silent = settings.silent;
combineSubdirectories = settings.combineSubdirectories;
ignore = settings.ignore;
flattenPaths = settings.flattenPaths;
premultiplyAlpha = settings.premultiplyAlpha;
square = settings.square;
useIndexes = settings.useIndexes;
bleed = settings.bleed;
bleedIterations = settings.bleedIterations;
limitMemory = settings.limitMemory;
grid = settings.grid;
scale = Arrays.copyOf(settings.scale, settings.scale.length);
scaleSuffix = Arrays.copyOf(settings.scaleSuffix, settings.scaleSuffix.length);
atlasExtension = settings.atlasExtension;
}
public String getScaledPackFileName (String packFileName, int scaleIndex) {
// Use suffix if not empty string.
if (scaleSuffix[scaleIndex].length() > 0)
packFileName += scaleSuffix[scaleIndex];
else {
// Otherwise if scale != 1 or multiple scales, use subdirectory.
float scaleValue = scale[scaleIndex];
if (scale.length != 1) {
packFileName = (scaleValue == (int)scaleValue ? Integer.toString((int)scaleValue) : Float.toString(scaleValue))
+ "/" + packFileName;
}
}
return packFileName;
}
}
/** Packs using defaults settings.
* @see TexturePacker#process(Settings, String, String, String) */
static public void process (String input, String output, String packFileName) {
process(new Settings(), input, output, packFileName);
}
/** @param input Directory containing individual images to be packed.
* @param output Directory where the pack file and page images will be written.
* @param packFileName The name of the pack file. Also used to name the page images. */
static public void process (Settings settings, String input, String output, String packFileName) {
try {
TexturePackerFileProcessor processor = new TexturePackerFileProcessor(settings, packFileName);
// Sort input files by name to avoid platform-dependent atlas output changes.
processor.setComparator(new Comparator<File>() {
public int compare (File file1, File file2) {
return file1.getName().compareTo(file2.getName());
}
});
processor.process(new File(input), new File(output));
} catch (Exception ex) {
throw new RuntimeException("Error packing images.", ex);
}
}
/** @return true if the output file does not yet exist or its last modification date is before the last modification date of
* the input file */
static public boolean isModified (String input, String output, String packFileName, Settings settings) {
String packFullFileName = output;
if (!packFullFileName.endsWith("/")) {
packFullFileName += "/";
}
// Check against the only file we know for sure will exist and will be changed if any asset changes:
// the atlas file
packFullFileName += packFileName;
packFullFileName += settings.atlasExtension;
File outputFile = new File(packFullFileName);
if (!outputFile.exists()) {
return true;
}
File inputFile = new File(input);
if (!inputFile.exists()) {
throw new IllegalArgumentException("Input file does not exist: " + inputFile.getAbsolutePath());
}
return isModified(inputFile, outputFile.lastModified());
}
static private boolean isModified (File file, long lastModified) {
if (file.lastModified() > lastModified) return true;
File[] children = file.listFiles();
if (children != null) {
for (File child : children)
if (isModified(child, lastModified)) return true;
}
return false;
}
static public boolean processIfModified (String input, String output, String packFileName) {
// Default settings (Needed to access the default atlas extension string)
Settings settings = new Settings();
if (isModified(input, output, packFileName, settings)) {
process(settings, input, output, packFileName);
return true;
}
return false;
}
static public boolean processIfModified (Settings settings, String input, String output, String packFileName) {
if (isModified(input, output, packFileName, settings)) {
process(settings, input, output, packFileName);
return true;
}
return false;
}
static public interface Packer {
public Array<Page> pack (Array<Rect> inputRects);
}
static final class InputImage {
File file;
String name;
BufferedImage image;
}
static public void main (String[] args) throws Exception {
Settings settings = null;
String input = null, output = null, packFileName = "pack.atlas";
switch (args.length) {
case 4:
settings = new Json().fromJson(Settings.class, new FileReader(args[3]));
case 3:
packFileName = args[2];
case 2:
output = args[1];
case 1:
input = args[0];
break;
default:
System.out.println("Usage: inputDir [outputDir] [packFileName] [settingsFileName]");
System.exit(0);
}
if (output == null) {
File inputFile = new File(input);
output = new File(inputFile.getParentFile(), inputFile.getName() + "-packed").getAbsolutePath();
}
if (settings == null) settings = new Settings();
process(settings, input, output, packFileName);
}
}