/*******************************************************************************
* 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.imagepacker;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
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;
/** @author Nathan Sweet */
public class TexturePacker2 {
private final Settings settings;
private final MaxRectsPacker maxRectsPacker;
private final ImageProcessor imageProcessor;
/** @param rootDir Can be null. */
public TexturePacker2 (File rootDir, Settings settings) {
this.settings = settings;
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);
maxRectsPacker = new MaxRectsPacker(settings);
imageProcessor = new ImageProcessor(rootDir, settings);
public TexturePacker2 (Settings settings) {
public void addImage (File file) {
imageProcessor.addImage(file);
public void addImage (BufferedImage image, String name) {
imageProcessor.addImage(image, name);
public void pack (File outputDir, String packFileName) {
if (packFileName.indexOf('.') == -1) packFileName += ".atlas";
Array<Page> pages = maxRectsPacker.pack(imageProcessor.getImages());
writeImages(outputDir, pages, packFileName);
writePackFile(outputDir, pages, packFileName);
} catch (IOException ex) {
throw new RuntimeException("Error writing pack file.", ex);
private void writeImages (File outputDir, Array<Page> pages, String packFileName) {
String imageName = packFileName;
int dotIndex = imageName.lastIndexOf('.');
if (dotIndex != -1) imageName = imageName.substring(0, dotIndex);
for (Page page : pages) {
int width = page.width, height = page.height;
int paddingX = settings.paddingX;
int paddingY = settings.paddingY;
if (settings.duplicatePadding) {
width -= settings.paddingX;
height -= settings.paddingY;
if (settings.edgePadding) {
width = MathUtils.nextPowerOfTwo(width);
height = MathUtils.nextPowerOfTwo(height);
width = Math.max(settings.minWidth, width);
height = Math.max(settings.minHeight, height);
if (settings.forceSquareOutput) {
outputFile = new File(outputDir, imageName + (fileIndex++ == 0 ? "" : fileIndex) + "." + settings.outputFormat);
if (!outputFile.exists()) break;
page.imageName = outputFile.getName();
BufferedImage canvas = new BufferedImage(width, height, getBufferedImageType(settings.format));
Graphics2D g = (Graphics2D)canvas.getGraphics();
System.out.println("Writing " + canvas.getWidth() + "x" + canvas.getHeight() + ": " + outputFile);
for (Rect rect : page.outputRects) {
BufferedImage image = rect.image;
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;
// 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));
// Copy corner pixels to fill corners of the padding.
for (int i = 1; i <= amountX; i++) {
for (int j = 1; j <= amountY; j++) {
canvas.setRGB(rectX - i, rectY - j, image.getRGB(0, 0));
canvas.setRGB(rectX - i, rectY + ih - 1 + j, image.getRGB(0, ih - 1));
canvas.setRGB(rectX + iw - 1 + i, rectY - j, image.getRGB(iw - 1, 0));
canvas.setRGB(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);
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")) {
canvas = new ColorBleedEffect().processImage(canvas, 2);
g = (Graphics2D)canvas.getGraphics();
g.setColor(Color.magenta);
g.drawRect(0, 0, width - 1, height - 1);
ImageOutputStream ios = null;
if (settings.outputFormat.equalsIgnoreCase("jpg")) {
BufferedImage newImage = new BufferedImage(canvas.getWidth(), canvas.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
newImage.getGraphics().drawImage(canvas, 0, 0, null);
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.write(null, new IIOImage(canvas, null, null), param);
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);
} catch (Exception ignored) {
private static 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);
private static void copy (BufferedImage src, int x, int y, int w, int h, BufferedImage dst, int dx, int dy, boolean rotated) {
for (int i = 0; i < w; i++)
for (int j = 0; j < h; j++)
dst.setRGB(dx + j, dy + w - i - 1, src.getRGB(x + i, y + j));
for (int i = 0; i < w; i++)
for (int j = 0; j < h; j++)
dst.setRGB(dx + i, dy + j, src.getRGB(x + i, y + j));
private void writePackFile (File outputDir, Array<Page> pages, String packFileName) throws IOException {
File packFile = new File(outputDir, packFileName);
// 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: "
FileWriter writer = new FileWriter(packFile, true);
// if (settings.jsonOutput) {
for (Page page : pages) {
writer.write("\n" + page.imageName + "\n");
writer.write("format: " + settings.format + "\n");
writer.write("filter: " + settings.filterMin + "," + settings.filterMag + "\n");
writer.write("repeat: " + getRepeatValue() + "\n");
for (Rect rect : page.outputRects) {
writeRect(writer, page, rect, rect.name);
for (Alias alias : rect.aliases) {
Rect aliasRect = new Rect();
writeRect(writer, page, aliasRect, alias.name);
private void writeRect (FileWriter 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.image.getWidth() + ", " + rect.image.getHeight() + "\n");
if (rect.splits != null) {
.write(" split: " + rect.splits[0] + ", " + rect.splits[1] + ", " + rect.splits[2] + ", " + rect.splits[3] + "\n");
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.image.getHeight() - 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";
private int getBufferedImageType (Format format) {
switch (settings.format) {
return BufferedImage.TYPE_INT_ARGB;
return BufferedImage.TYPE_INT_RGB;
return BufferedImage.TYPE_BYTE_GRAY;
throw new RuntimeException("Unsupported format: " + settings.format);
/** @author Nathan Sweet */
public static class Page {
public Array<Rect> outputRects, remainingRects;
public int x, y, width, height;
* @author Nathan Sweet */
public static class Alias {
public int offsetX, offsetY, originalWidth, originalHeight;
public Alias (Rect rect) {
originalWidth = rect.originalWidth;
originalHeight = rect.originalHeight;
public void apply (Rect rect) {
rect.originalWidth = originalWidth;
rect.originalHeight = originalHeight;
/** @author Nathan Sweet */
public static class Rect {
public BufferedImage image;
public int offsetX, offsetY, originalWidth, originalHeight;
public int x, y, width, height;
public Set<Alias> aliases = new HashSet<Alias>();
public boolean canRotate = true;
Rect (BufferedImage source, int left, int top, int newWidth, int newHeight) {
image = new BufferedImage(source.getColorModel(), source.getRaster().createWritableChild(left, top, newWidth, newHeight,
0, 0, null), source.getColorModel().isAlphaPremultiplied(), null);
originalWidth = source.getWidth();
originalHeight = source.getHeight();
void setSize (Rect rect) {
originalWidth = rect.originalWidth;
originalHeight = rect.originalHeight;
canRotate = rect.canRotate;
public boolean equals (Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
if (other.name != null) return false;
} else if (!name.equals(other.name)) return false;
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 int minWidth = 16, minHeight = 16;
public int maxWidth = 1024, maxHeight = 1024;
public boolean forceSquareOutput = 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 combineSubdirectories;
public boolean flattenPaths;
public boolean premultiplyAlpha;
public boolean useIndexes = true;
public boolean bleed = true;
public Settings (Settings settings) {
rotation = settings.rotation;
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;
format = settings.format;
jpegQuality = settings.jpegQuality;
outputFormat = settings.outputFormat;
filterMin = settings.filterMin;
filterMag = settings.filterMag;
combineSubdirectories = settings.combineSubdirectories;
flattenPaths = settings.flattenPaths;
premultiplyAlpha = settings.premultiplyAlpha;
forceSquareOutput = settings.forceSquareOutput;
useIndexes = settings.useIndexes;
static public void process (String input, String output, String packFileName) {
new TexturePackerFileProcessor(new Settings(), packFileName).process(new File(input), new File(output));
throw new RuntimeException("Error packing files.", ex);
static public void process (Settings settings, String input, String output, String packFileName) {
new TexturePackerFileProcessor(settings, packFileName).process(new File(input), new File(output));
throw new RuntimeException("Error packing files.", ex);
/** @return true if the output file does not yet exist or its last modification date is before the last modification date of the
static public boolean isModified (String input, String output, String packFileName) {
String packFullFileName = output;
if (!packFullFileName.endsWith("/")) packFullFileName += "/";
packFullFileName += packFileName;
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 inputFile.lastModified() > outputFile.lastModified();
static public void processIfModified (String input, String output, String packFileName) {
if (isModified(input, output, packFileName)) process(input, output, packFileName);
static public void processIfModified (Settings settings, String input, String output, String packFileName) {
if (isModified(input, output, packFileName)) process(settings, input, output, packFileName);