• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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. */
TexturePacker(File rootDir, Settings settings)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 
TexturePacker(Settings settings)78 	public TexturePacker (Settings settings) {
79 		this(null, settings);
80 	}
81 
addImage(File file)82 	public void addImage (File file) {
83 		InputImage inputImage = new InputImage();
84 		inputImage.file = file;
85 		inputImages.add(inputImage);
86 	}
87 
addImage(BufferedImage image, String name)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 
pack(File outputDir, String packFileName)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 
writeImages(File outputDir, String scaledPackFileName, Array<Page> pages)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 
plot(BufferedImage dst, int x, int y, int argb)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 
copy(BufferedImage src, int x, int y, int w, int h, BufferedImage dst, int dx, int dy, boolean rotated)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 
writePackFile(File outputDir, String scaledPackFileName, Array<Page> pages)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 
writeRect(Writer writer, Page page, Rect rect, String name)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 
getRepeatValue()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 
getBufferedImageType(Format format)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 
Alias(Rect rect)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 
apply(Rect rect)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 
compareTo(Alias o)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 
Rect(BufferedImage source, int left, int top, int newWidth, int newHeight, boolean isPatch)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)}. */
unloadImage(File file)446 		public void unloadImage (File file) {
447 			this.file = file;
448 			image = null;
449 		}
450 
getImage(ImageProcessor imageProcessor)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 
Rect()466 		Rect () {
467 		}
468 
Rect(Rect rect)469 		Rect (Rect rect) {
470 			x = rect.x;
471 			y = rect.y;
472 			width = rect.width;
473 			height = rect.height;
474 		}
475 
set(Rect rect)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 
compareTo(Rect o)501 		public int compareTo (Rect o) {
502 			return name.compareTo(o.name);
503 		}
504 
505 		@Override
equals(Object obj)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
toString()518 		public String toString () {
519 			return name + "[" + x + "," + y + " " + width + "x" + height + "]";
520 		}
521 
getAtlasName(String name, boolean flattenPaths)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 
Settings()560 		public Settings () {
561 		}
562 
Settings(Settings settings)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 
getScaledPackFileName(String packFileName, int scaleIndex)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) */
process(String input, String output, String packFileName)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. */
process(Settings settings, String input, String output, String packFileName)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 */
isModified(String input, String output, String packFileName, Settings settings)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 
processIfModified(String input, String output, String packFileName)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 
processIfModified(Settings settings, String input, String output, String packFileName)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 {
pack(Array<Rect> inputRects)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 
main(String[] args)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