Home | History | Annotate | Download | only in texturepacker
      1 /*******************************************************************************
      2  * Copyright 2011 See AUTHORS file.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *   http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  ******************************************************************************/
     16 
     17 package com.badlogic.gdx.tools.texturepacker;
     18 
     19 import java.awt.Color;
     20 import java.awt.Graphics2D;
     21 import java.awt.image.BufferedImage;
     22 import java.io.File;
     23 import java.io.FileOutputStream;
     24 import java.io.FileReader;
     25 import java.io.FileWriter;
     26 import java.io.IOException;
     27 import java.io.OutputStreamWriter;
     28 import java.io.Writer;
     29 import java.util.Comparator;
     30 import java.util.HashSet;
     31 import java.util.Iterator;
     32 import java.util.Set;
     33 
     34 import javax.imageio.IIOImage;
     35 import javax.imageio.ImageIO;
     36 import javax.imageio.ImageWriteParam;
     37 import javax.imageio.ImageWriter;
     38 import javax.imageio.stream.ImageOutputStream;
     39 
     40 import com.badlogic.gdx.files.FileHandle;
     41 import com.badlogic.gdx.graphics.Pixmap.Format;
     42 import com.badlogic.gdx.graphics.Texture.TextureFilter;
     43 import com.badlogic.gdx.graphics.Texture.TextureWrap;
     44 import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
     45 import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData.Region;
     46 import com.badlogic.gdx.math.MathUtils;
     47 import com.badlogic.gdx.utils.Array;
     48 import com.badlogic.gdx.utils.GdxRuntimeException;
     49 import com.badlogic.gdx.utils.Json;
     50 
     51 /** @author Nathan Sweet */
     52 public class TexturePacker {
     53 	private final Settings settings;
     54 	private final Packer packer;
     55 	private final ImageProcessor imageProcessor;
     56 	private final Array<InputImage> inputImages = new Array();
     57 	private File rootDir;
     58 
     59 	/** @param rootDir Used to strip the root directory prefix from image file names, can be null. */
     60 	public TexturePacker (File rootDir, Settings settings) {
     61 		this.rootDir = rootDir;
     62 		this.settings = settings;
     63 
     64 		if (settings.pot) {
     65 			if (settings.maxWidth != MathUtils.nextPowerOfTwo(settings.maxWidth))
     66 				throw new RuntimeException("If pot is true, maxWidth must be a power of two: " + settings.maxWidth);
     67 			if (settings.maxHeight != MathUtils.nextPowerOfTwo(settings.maxHeight))
     68 				throw new RuntimeException("If pot is true, maxHeight must be a power of two: " + settings.maxHeight);
     69 		}
     70 
     71 		if (settings.grid)
     72 			packer = new GridPacker(settings);
     73 		else
     74 			packer = new MaxRectsPacker(settings);
     75 		imageProcessor = new ImageProcessor(rootDir, settings);
     76 	}
     77 
     78 	public TexturePacker (Settings settings) {
     79 		this(null, settings);
     80 	}
     81 
     82 	public void addImage (File file) {
     83 		InputImage inputImage = new InputImage();
     84 		inputImage.file = file;
     85 		inputImages.add(inputImage);
     86 	}
     87 
     88 	public void addImage (BufferedImage image, String name) {
     89 		InputImage inputImage = new InputImage();
     90 		inputImage.image = image;
     91 		inputImage.name = name;
     92 		inputImages.add(inputImage);
     93 	}
     94 
     95 	public void pack (File outputDir, String packFileName) {
     96 		if (packFileName.endsWith(settings.atlasExtension))
     97 			packFileName = packFileName.substring(0, packFileName.length() - settings.atlasExtension.length());
     98 		outputDir.mkdirs();
     99 
    100 		for (int i = 0, n = settings.scale.length; i < n; i++) {
    101 			imageProcessor.setScale(settings.scale[i]);
    102 			for (InputImage inputImage : inputImages) {
    103 				if (inputImage.file != null)
    104 					imageProcessor.addImage(inputImage.file);
    105 				else
    106 					imageProcessor.addImage(inputImage.image, inputImage.name);
    107 			}
    108 
    109 			Array<Page> pages = packer.pack(imageProcessor.getImages());
    110 
    111 			String scaledPackFileName = settings.getScaledPackFileName(packFileName, i);
    112 			writeImages(outputDir, scaledPackFileName, pages);
    113 			try {
    114 				writePackFile(outputDir, scaledPackFileName, pages);
    115 			} catch (IOException ex) {
    116 				throw new RuntimeException("Error writing pack file.", ex);
    117 			}
    118 			imageProcessor.clear();
    119 		}
    120 	}
    121 
    122 	private void writeImages (File outputDir, String scaledPackFileName, Array<Page> pages) {
    123 		File packFileNoExt = new File(outputDir, scaledPackFileName);
    124 		File packDir = packFileNoExt.getParentFile();
    125 		String imageName = packFileNoExt.getName();
    126 
    127 		int fileIndex = 0;
    128 		for (Page page : pages) {
    129 			int width = page.width, height = page.height;
    130 			int paddingX = settings.paddingX;
    131 			int paddingY = settings.paddingY;
    132 			if (settings.duplicatePadding) {
    133 				paddingX /= 2;
    134 				paddingY /= 2;
    135 			}
    136 			width -= settings.paddingX;
    137 			height -= settings.paddingY;
    138 			if (settings.edgePadding) {
    139 				page.x = paddingX;
    140 				page.y = paddingY;
    141 				width += paddingX * 2;
    142 				height += paddingY * 2;
    143 			}
    144 			if (settings.pot) {
    145 				width = MathUtils.nextPowerOfTwo(width);
    146 				height = MathUtils.nextPowerOfTwo(height);
    147 			}
    148 			width = Math.max(settings.minWidth, width);
    149 			height = Math.max(settings.minHeight, height);
    150 			page.imageWidth = width;
    151 			page.imageHeight = height;
    152 
    153 			File outputFile;
    154 			while (true) {
    155 				outputFile = new File(packDir, imageName + (fileIndex++ == 0 ? "" : fileIndex) + "." + settings.outputFormat);
    156 				if (!outputFile.exists()) break;
    157 			}
    158 			new FileHandle(outputFile).parent().mkdirs();
    159 			page.imageName = outputFile.getName();
    160 
    161 			BufferedImage canvas = new BufferedImage(width, height, getBufferedImageType(settings.format));
    162 			Graphics2D g = (Graphics2D)canvas.getGraphics();
    163 
    164 			if (!settings.silent) System.out.println("Writing " + canvas.getWidth() + "x" + canvas.getHeight() + ": " + outputFile);
    165 
    166 			for (Rect rect : page.outputRects) {
    167 				BufferedImage image = rect.getImage(imageProcessor);
    168 				int iw = image.getWidth();
    169 				int ih = image.getHeight();
    170 				int rectX = page.x + rect.x, rectY = page.y + page.height - rect.y - rect.height;
    171 				if (settings.duplicatePadding) {
    172 					int amountX = settings.paddingX / 2;
    173 					int amountY = settings.paddingY / 2;
    174 					if (rect.rotated) {
    175 						// Copy corner pixels to fill corners of the padding.
    176 						for (int i = 1; i <= amountX; i++) {
    177 							for (int j = 1; j <= amountY; j++) {
    178 								plot(canvas, rectX - j, rectY + iw - 1 + i, image.getRGB(0, 0));
    179 								plot(canvas, rectX + ih - 1 + j, rectY + iw - 1 + i, image.getRGB(0, ih - 1));
    180 								plot(canvas, rectX - j, rectY - i, image.getRGB(iw - 1, 0));
    181 								plot(canvas, rectX + ih - 1 + j, rectY - i, image.getRGB(iw - 1, ih - 1));
    182 							}
    183 						}
    184 						// Copy edge pixels into padding.
    185 						for (int i = 1; i <= amountY; i++) {
    186 							for (int j = 0; j < iw; j++) {
    187 								plot(canvas, rectX - i, rectY + iw - 1 - j, image.getRGB(j, 0));
    188 								plot(canvas, rectX + ih - 1 + i, rectY + iw - 1 - j, image.getRGB(j, ih - 1));
    189 							}
    190 						}
    191 						for (int i = 1; i <= amountX; i++) {
    192 							for (int j = 0; j < ih; j++) {
    193 								plot(canvas, rectX + j, rectY - i, image.getRGB(iw - 1, j));
    194 								plot(canvas, rectX + j, rectY + iw - 1 + i, image.getRGB(0, j));
    195 							}
    196 						}
    197 					} else {
    198 						// Copy corner pixels to fill corners of the padding.
    199 						for (int i = 1; i <= amountX; i++) {
    200 							for (int j = 1; j <= amountY; j++) {
    201 								plot(canvas, rectX - i, rectY - j, image.getRGB(0, 0));
    202 								plot(canvas, rectX - i, rectY + ih - 1 + j, image.getRGB(0, ih - 1));
    203 								plot(canvas, rectX + iw - 1 + i, rectY - j, image.getRGB(iw - 1, 0));
    204 								plot(canvas, rectX + iw - 1 + i, rectY + ih - 1 + j, image.getRGB(iw - 1, ih - 1));
    205 							}
    206 						}
    207 						// Copy edge pixels into padding.
    208 						for (int i = 1; i <= amountY; i++) {
    209 							copy(image, 0, 0, iw, 1, canvas, rectX, rectY - i, rect.rotated);
    210 							copy(image, 0, ih - 1, iw, 1, canvas, rectX, rectY + ih - 1 + i, rect.rotated);
    211 						}
    212 						for (int i = 1; i <= amountX; i++) {
    213 							copy(image, 0, 0, 1, ih, canvas, rectX - i, rectY, rect.rotated);
    214 							copy(image, iw - 1, 0, 1, ih, canvas, rectX + iw - 1 + i, rectY, rect.rotated);
    215 						}
    216 					}
    217 				}
    218 				copy(image, 0, 0, iw, ih, canvas, rectX, rectY, rect.rotated);
    219 				if (settings.debug) {
    220 					g.setColor(Color.magenta);
    221 					g.drawRect(rectX, rectY, rect.width - settings.paddingX - 1, rect.height - settings.paddingY - 1);
    222 				}
    223 			}
    224 
    225 			if (settings.bleed && !settings.premultiplyAlpha && !(settings.outputFormat.equalsIgnoreCase("jpg") || settings.outputFormat.equalsIgnoreCase("jpeg"))) {
    226 				canvas = new ColorBleedEffect().processImage(canvas, 2);
    227 				g = (Graphics2D)canvas.getGraphics();
    228 			}
    229 
    230 			if (settings.debug) {
    231 				g.setColor(Color.magenta);
    232 				g.drawRect(0, 0, width - 1, height - 1);
    233 			}
    234 
    235 			ImageOutputStream ios = null;
    236 			try {
    237 				if (settings.outputFormat.equalsIgnoreCase("jpg") || settings.outputFormat.equalsIgnoreCase("jpeg")) {
    238 					BufferedImage newImage = new BufferedImage(canvas.getWidth(), canvas.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
    239 					newImage.getGraphics().drawImage(canvas, 0, 0, null);
    240 					canvas = newImage;
    241 
    242 					Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
    243 					ImageWriter writer = writers.next();
    244 					ImageWriteParam param = writer.getDefaultWriteParam();
    245 					param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    246 					param.setCompressionQuality(settings.jpegQuality);
    247 					ios = ImageIO.createImageOutputStream(outputFile);
    248 					writer.setOutput(ios);
    249 					writer.write(null, new IIOImage(canvas, null, null), param);
    250 				} else {
    251 					if (settings.premultiplyAlpha) canvas.getColorModel().coerceData(canvas.getRaster(), true);
    252 					ImageIO.write(canvas, "png", outputFile);
    253 				}
    254 			} catch (IOException ex) {
    255 				throw new RuntimeException("Error writing file: " + outputFile, ex);
    256 			} finally {
    257 				if (ios != null) {
    258 					try {
    259 						ios.close();
    260 					} catch (Exception ignored) {
    261 					}
    262 				}
    263 			}
    264 		}
    265 	}
    266 
    267 	static private void plot (BufferedImage dst, int x, int y, int argb) {
    268 		if (0 <= x && x < dst.getWidth() && 0 <= y && y < dst.getHeight()) dst.setRGB(x, y, argb);
    269 	}
    270 
    271 	static private void copy (BufferedImage src, int x, int y, int w, int h, BufferedImage dst, int dx, int dy, boolean rotated) {
    272 		if (rotated) {
    273 			for (int i = 0; i < w; i++)
    274 				for (int j = 0; j < h; j++)
    275 					plot(dst, dx + j, dy + w - i - 1, src.getRGB(x + i, y + j));
    276 		} else {
    277 			for (int i = 0; i < w; i++)
    278 				for (int j = 0; j < h; j++)
    279 					plot(dst, dx + i, dy + j, src.getRGB(x + i, y + j));
    280 		}
    281 	}
    282 
    283 	private void writePackFile (File outputDir, String scaledPackFileName, Array<Page> pages) throws IOException {
    284 		File packFile = new File(outputDir, scaledPackFileName + settings.atlasExtension);
    285 		File packDir = packFile.getParentFile();
    286 		packDir.mkdirs();
    287 
    288 		if (packFile.exists()) {
    289 			// Make sure there aren't duplicate names.
    290 			TextureAtlasData textureAtlasData = new TextureAtlasData(new FileHandle(packFile), new FileHandle(packFile), false);
    291 			for (Page page : pages) {
    292 				for (Rect rect : page.outputRects) {
    293 					String rectName = Rect.getAtlasName(rect.name, settings.flattenPaths);
    294 					for (Region region : textureAtlasData.getRegions()) {
    295 						if (region.name.equals(rectName)) {
    296 							throw new GdxRuntimeException("A region with the name \"" + rectName + "\" has already been packed: "
    297 								+ rect.name);
    298 						}
    299 					}
    300 				}
    301 			}
    302 		}
    303 
    304 		Writer writer = new OutputStreamWriter(new FileOutputStream(packFile, true), "UTF-8");
    305 		for (Page page : pages) {
    306 			writer.write("\n" + page.imageName + "\n");
    307 			writer.write("size: " + page.imageWidth + "," + page.imageHeight + "\n");
    308 			writer.write("format: " + settings.format + "\n");
    309 			writer.write("filter: " + settings.filterMin + "," + settings.filterMag + "\n");
    310 			writer.write("repeat: " + getRepeatValue() + "\n");
    311 
    312 			page.outputRects.sort();
    313 			for (Rect rect : page.outputRects) {
    314 				writeRect(writer, page, rect, rect.name);
    315 				Array<Alias> aliases = new Array(rect.aliases.toArray());
    316 				aliases.sort();
    317 				for (Alias alias : aliases) {
    318 					Rect aliasRect = new Rect();
    319 					aliasRect.set(rect);
    320 					alias.apply(aliasRect);
    321 					writeRect(writer, page, aliasRect, alias.name);
    322 				}
    323 			}
    324 		}
    325 		writer.close();
    326 	}
    327 
    328 	private void writeRect (Writer writer, Page page, Rect rect, String name) throws IOException {
    329 		writer.write(Rect.getAtlasName(name, settings.flattenPaths) + "\n");
    330 		writer.write("  rotate: " + rect.rotated + "\n");
    331 		writer.write("  xy: " + (page.x + rect.x) + ", " + (page.y + page.height - rect.height - rect.y) + "\n");
    332 
    333 		writer.write("  size: " + rect.regionWidth + ", " + rect.regionHeight + "\n");
    334 		if (rect.splits != null) {
    335 			writer.write("  split: " //
    336 				+ rect.splits[0] + ", " + rect.splits[1] + ", " + rect.splits[2] + ", " + rect.splits[3] + "\n");
    337 		}
    338 		if (rect.pads != null) {
    339 			if (rect.splits == null) writer.write("  split: 0, 0, 0, 0\n");
    340 			writer.write("  pad: " + rect.pads[0] + ", " + rect.pads[1] + ", " + rect.pads[2] + ", " + rect.pads[3] + "\n");
    341 		}
    342 		writer.write("  orig: " + rect.originalWidth + ", " + rect.originalHeight + "\n");
    343 		writer.write("  offset: " + rect.offsetX + ", " + (rect.originalHeight - rect.regionHeight - rect.offsetY) + "\n");
    344 		writer.write("  index: " + rect.index + "\n");
    345 	}
    346 
    347 	private String getRepeatValue () {
    348 		if (settings.wrapX == TextureWrap.Repeat && settings.wrapY == TextureWrap.Repeat) return "xy";
    349 		if (settings.wrapX == TextureWrap.Repeat && settings.wrapY == TextureWrap.ClampToEdge) return "x";
    350 		if (settings.wrapX == TextureWrap.ClampToEdge && settings.wrapY == TextureWrap.Repeat) return "y";
    351 		return "none";
    352 	}
    353 
    354 	private int getBufferedImageType (Format format) {
    355 		switch (settings.format) {
    356 		case RGBA8888:
    357 		case RGBA4444:
    358 			return BufferedImage.TYPE_INT_ARGB;
    359 		case RGB565:
    360 		case RGB888:
    361 			return BufferedImage.TYPE_INT_RGB;
    362 		case Alpha:
    363 			return BufferedImage.TYPE_BYTE_GRAY;
    364 		default:
    365 			throw new RuntimeException("Unsupported format: " + settings.format);
    366 		}
    367 	}
    368 
    369 	/** @author Nathan Sweet */
    370 	static public class Page {
    371 		public String imageName;
    372 		public Array<Rect> outputRects, remainingRects;
    373 		public float occupancy;
    374 		public int x, y, width, height, imageWidth, imageHeight;
    375 	}
    376 
    377 	/** @author Regnarock
    378 	 * @author Nathan Sweet */
    379 	static public class Alias implements Comparable<Alias> {
    380 		public String name;
    381 		public int index;
    382 		public int[] splits;
    383 		public int[] pads;
    384 		public int offsetX, offsetY, originalWidth, originalHeight;
    385 
    386 		public Alias (Rect rect) {
    387 			name = rect.name;
    388 			index = rect.index;
    389 			splits = rect.splits;
    390 			pads = rect.pads;
    391 			offsetX = rect.offsetX;
    392 			offsetY = rect.offsetY;
    393 			originalWidth = rect.originalWidth;
    394 			originalHeight = rect.originalHeight;
    395 		}
    396 
    397 		public void apply (Rect rect) {
    398 			rect.name = name;
    399 			rect.index = index;
    400 			rect.splits = splits;
    401 			rect.pads = pads;
    402 			rect.offsetX = offsetX;
    403 			rect.offsetY = offsetY;
    404 			rect.originalWidth = originalWidth;
    405 			rect.originalHeight = originalHeight;
    406 		}
    407 
    408 		public int compareTo (Alias o) {
    409 			return name.compareTo(o.name);
    410 		}
    411 	}
    412 
    413 	/** @author Nathan Sweet */
    414 	static public class Rect implements Comparable<Rect> {
    415 		public String name;
    416 		public int offsetX, offsetY, regionWidth, regionHeight, originalWidth, originalHeight;
    417 		public int x, y;
    418 		public int width, height; // Portion of page taken by this region, including padding.
    419 		public int index;
    420 		public boolean rotated;
    421 		public Set<Alias> aliases = new HashSet<Alias>();
    422 		public int[] splits;
    423 		public int[] pads;
    424 		public boolean canRotate = true;
    425 
    426 		private boolean isPatch;
    427 		private BufferedImage image;
    428 		private File file;
    429 		int score1, score2;
    430 
    431 		Rect (BufferedImage source, int left, int top, int newWidth, int newHeight, boolean isPatch) {
    432 			image = new BufferedImage(source.getColorModel(), source.getRaster().createWritableChild(left, top, newWidth, newHeight,
    433 				0, 0, null), source.getColorModel().isAlphaPremultiplied(), null);
    434 			offsetX = left;
    435 			offsetY = top;
    436 			regionWidth = newWidth;
    437 			regionHeight = newHeight;
    438 			originalWidth = source.getWidth();
    439 			originalHeight = source.getHeight();
    440 			width = newWidth;
    441 			height = newHeight;
    442 			this.isPatch = isPatch;
    443 		}
    444 
    445 		/** Clears the image for this rect, which will be loaded from the specified file by {@link #getImage(ImageProcessor)}. */
    446 		public void unloadImage (File file) {
    447 			this.file = file;
    448 			image = null;
    449 		}
    450 
    451 		public BufferedImage getImage (ImageProcessor imageProcessor) {
    452 			if (image != null) return image;
    453 
    454 			BufferedImage image;
    455 			try {
    456 				image = ImageIO.read(file);
    457 			} catch (IOException ex) {
    458 				throw new RuntimeException("Error reading image: " + file, ex);
    459 			}
    460 			if (image == null) throw new RuntimeException("Unable to read image: " + file);
    461 			String name = this.name;
    462 			if (isPatch) name += ".9";
    463 			return imageProcessor.processImage(image, name).getImage(null);
    464 		}
    465 
    466 		Rect () {
    467 		}
    468 
    469 		Rect (Rect rect) {
    470 			x = rect.x;
    471 			y = rect.y;
    472 			width = rect.width;
    473 			height = rect.height;
    474 		}
    475 
    476 		void set (Rect rect) {
    477 			name = rect.name;
    478 			image = rect.image;
    479 			offsetX = rect.offsetX;
    480 			offsetY = rect.offsetY;
    481 			regionWidth = rect.regionWidth;
    482 			regionHeight = rect.regionHeight;
    483 			originalWidth = rect.originalWidth;
    484 			originalHeight = rect.originalHeight;
    485 			x = rect.x;
    486 			y = rect.y;
    487 			width = rect.width;
    488 			height = rect.height;
    489 			index = rect.index;
    490 			rotated = rect.rotated;
    491 			aliases = rect.aliases;
    492 			splits = rect.splits;
    493 			pads = rect.pads;
    494 			canRotate = rect.canRotate;
    495 			score1 = rect.score1;
    496 			score2 = rect.score2;
    497 			file = rect.file;
    498 			isPatch = rect.isPatch;
    499 		}
    500 
    501 		public int compareTo (Rect o) {
    502 			return name.compareTo(o.name);
    503 		}
    504 
    505 		@Override
    506 		public boolean equals (Object obj) {
    507 			if (this == obj) return true;
    508 			if (obj == null) return false;
    509 			if (getClass() != obj.getClass()) return false;
    510 			Rect other = (Rect)obj;
    511 			if (name == null) {
    512 				if (other.name != null) return false;
    513 			} else if (!name.equals(other.name)) return false;
    514 			return true;
    515 		}
    516 
    517 		@Override
    518 		public String toString () {
    519 			return name + "[" + x + "," + y + " " + width + "x" + height + "]";
    520 		}
    521 
    522 		static public String getAtlasName (String name, boolean flattenPaths) {
    523 			return flattenPaths ? new FileHandle(name).name() : name;
    524 		}
    525 	}
    526 
    527 	/** @author Nathan Sweet */
    528 	static public class Settings {
    529 		public boolean pot = true;
    530 		public int paddingX = 2, paddingY = 2;
    531 		public boolean edgePadding = true;
    532 		public boolean duplicatePadding = false;
    533 		public boolean rotation;
    534 		public int minWidth = 16, minHeight = 16;
    535 		public int maxWidth = 1024, maxHeight = 1024;
    536 		public boolean square = false;
    537 		public boolean stripWhitespaceX, stripWhitespaceY;
    538 		public int alphaThreshold;
    539 		public TextureFilter filterMin = TextureFilter.Nearest, filterMag = TextureFilter.Nearest;
    540 		public TextureWrap wrapX = TextureWrap.ClampToEdge, wrapY = TextureWrap.ClampToEdge;
    541 		public Format format = Format.RGBA8888;
    542 		public boolean alias = true;
    543 		public String outputFormat = "png";
    544 		public float jpegQuality = 0.9f;
    545 		public boolean ignoreBlankImages = true;
    546 		public boolean fast;
    547 		public boolean debug;
    548 		public boolean silent;
    549 		public boolean combineSubdirectories;
    550 		public boolean flattenPaths;
    551 		public boolean premultiplyAlpha;
    552 		public boolean useIndexes = true;
    553 		public boolean bleed = true;
    554 		public boolean limitMemory = true;
    555 		public boolean grid;
    556 		public float[] scale = {1};
    557 		public String[] scaleSuffix = {""};
    558 		public String atlasExtension = ".atlas";
    559 
    560 		public Settings () {
    561 		}
    562 
    563 		public Settings (Settings settings) {
    564 			fast = settings.fast;
    565 			rotation = settings.rotation;
    566 			pot = settings.pot;
    567 			minWidth = settings.minWidth;
    568 			minHeight = settings.minHeight;
    569 			maxWidth = settings.maxWidth;
    570 			maxHeight = settings.maxHeight;
    571 			paddingX = settings.paddingX;
    572 			paddingY = settings.paddingY;
    573 			edgePadding = settings.edgePadding;
    574 			duplicatePadding = settings.duplicatePadding;
    575 			alphaThreshold = settings.alphaThreshold;
    576 			ignoreBlankImages = settings.ignoreBlankImages;
    577 			stripWhitespaceX = settings.stripWhitespaceX;
    578 			stripWhitespaceY = settings.stripWhitespaceY;
    579 			alias = settings.alias;
    580 			format = settings.format;
    581 			jpegQuality = settings.jpegQuality;
    582 			outputFormat = settings.outputFormat;
    583 			filterMin = settings.filterMin;
    584 			filterMag = settings.filterMag;
    585 			wrapX = settings.wrapX;
    586 			wrapY = settings.wrapY;
    587 			debug = settings.debug;
    588 			silent = settings.silent;
    589 			combineSubdirectories = settings.combineSubdirectories;
    590 			flattenPaths = settings.flattenPaths;
    591 			premultiplyAlpha = settings.premultiplyAlpha;
    592 			square = settings.square;
    593 			useIndexes = settings.useIndexes;
    594 			bleed = settings.bleed;
    595 			limitMemory = settings.limitMemory;
    596 			grid = settings.grid;
    597 			scale = settings.scale;
    598 			scaleSuffix = settings.scaleSuffix;
    599 			atlasExtension = settings.atlasExtension;
    600 		}
    601 
    602 		public String getScaledPackFileName (String packFileName, int scaleIndex) {
    603 			// Use suffix if not empty string.
    604 			if (scaleSuffix[scaleIndex].length() > 0)
    605 				packFileName += scaleSuffix[scaleIndex];
    606 			else {
    607 				// Otherwise if scale != 1 or multiple scales, use subdirectory.
    608 				float scaleValue = scale[scaleIndex];
    609 				if (scale.length != 1) {
    610 					packFileName = (scaleValue == (int)scaleValue ? Integer.toString((int)scaleValue) : Float.toString(scaleValue))
    611 						+ "/" + packFileName;
    612 				}
    613 			}
    614 			return packFileName;
    615 		}
    616 	}
    617 
    618 	/** Packs using defaults settings.
    619 	 * @see TexturePacker#process(Settings, String, String, String) */
    620 	static public void process (String input, String output, String packFileName) {
    621 		process(new Settings(), input, output, packFileName);
    622 	}
    623 
    624 	/** @param input Directory containing individual images to be packed.
    625 	 * @param output Directory where the pack file and page images will be written.
    626 	 * @param packFileName The name of the pack file. Also used to name the page images. */
    627 	static public void process (Settings settings, String input, String output, String packFileName) {
    628 		try {
    629 			TexturePackerFileProcessor processor = new TexturePackerFileProcessor(settings, packFileName);
    630 			// Sort input files by name to avoid platform-dependent atlas output changes.
    631 			processor.setComparator(new Comparator<File>() {
    632 				public int compare (File file1, File file2) {
    633 					return file1.getName().compareTo(file2.getName());
    634 				}
    635 			});
    636 			processor.process(new File(input), new File(output));
    637 		} catch (Exception ex) {
    638 			throw new RuntimeException("Error packing images.", ex);
    639 		}
    640 	}
    641 
    642 	/** @return true if the output file does not yet exist or its last modification date is before the last modification date of the
    643 	 *         input file */
    644 	static public boolean isModified (String input, String output, String packFileName, Settings settings) {
    645 		String packFullFileName = output;
    646 
    647 		if (!packFullFileName.endsWith("/")) {
    648 			packFullFileName += "/";
    649 		}
    650 
    651 		// Check against the only file we know for sure will exist and will be changed if any asset changes:
    652 		// the atlas file
    653 		packFullFileName += packFileName;
    654 		packFullFileName += settings.atlasExtension;
    655 		File outputFile = new File(packFullFileName);
    656 
    657 		if (!outputFile.exists()) {
    658 			return true;
    659 		}
    660 
    661 		File inputFile = new File(input);
    662 		if (!inputFile.exists()) {
    663 			throw new IllegalArgumentException("Input file does not exist: " + inputFile.getAbsolutePath());
    664 		}
    665 
    666 		return inputFile.lastModified() > outputFile.lastModified();
    667 	}
    668 
    669 	static public void processIfModified (String input, String output, String packFileName) {
    670 		// Default settings (Needed to access the default atlas extension string)
    671 		Settings settings = new Settings();
    672 
    673 		if (isModified(input, output, packFileName, settings)) {
    674 			process(settings, input, output, packFileName);
    675 		}
    676 	}
    677 
    678 	static public void processIfModified (Settings settings, String input, String output, String packFileName) {
    679 		if (isModified(input, output, packFileName, settings)) {
    680 			process(settings, input, output, packFileName);
    681 		}
    682 	}
    683 
    684 	static public interface Packer {
    685 		public Array<Page> pack (Array<Rect> inputRects);
    686 	}
    687 
    688 	static final class InputImage {
    689 		File file;
    690 		String name;
    691 		BufferedImage image;
    692 	}
    693 
    694 	static public void main (String[] args) throws Exception {
    695 		Settings settings = null;
    696 		String input = null, output = null, packFileName = "pack.atlas";
    697 
    698 		switch (args.length) {
    699 		case 4:
    700 			settings = new Json().fromJson(Settings.class, new FileReader(args[3]));
    701 		case 3:
    702 			packFileName = args[2];
    703 		case 2:
    704 			output = args[1];
    705 		case 1:
    706 			input = args[0];
    707 			break;
    708 		default:
    709 			System.out.println("Usage: inputDir [outputDir] [packFileName] [settingsFileName]");
    710 			System.exit(0);
    711 		}
    712 
    713 		if (output == null) {
    714 			File inputFile = new File(input);
    715 			output = new File(inputFile.getParentFile(), inputFile.getName() + "-packed").getAbsolutePath();
    716 		}
    717 		if (settings == null)
    718 			settings = new Settings();
    719 
    720 		process(settings, input, output, packFileName);
    721 	}
    722 }
    723