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