/*******************************************************************************
* 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.graphics.g2d;
import static com.badlogic.gdx.graphics.Texture.TextureWrap.ClampToEdge;
import static com.badlogic.gdx.graphics.Texture.TextureWrap.Repeat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import com.badlogic.gdx.Files.FileType;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData.Page;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData.Region;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
/**
* Loads images from texture atlases created by TexturePacker.<br>
* <br>
* A TextureAtlas must be disposed to free up the resources consumed by the backing textures.
*
* @author Nathan Sweet
*/
public class TextureAtlas implements Disposable {
static final String[] tuple = new String[4];
private final HashSet<Texture> textures = new HashSet(4);
private final Array<AtlasRegion> regions = new Array<AtlasRegion>();
public static class TextureAtlasData {
public static class Page {
public final FileHandle textureFile;
public Texture texture;
public final boolean useMipMaps;
public final Format format;
public final TextureFilter minFilter;
public final TextureFilter magFilter;
public final TextureWrap uWrap;
public final TextureWrap vWrap;
public Page(FileHandle handle, boolean useMipMaps, Format format, TextureFilter minFilter,
TextureFilter magFilter, TextureWrap uWrap, TextureWrap vWrap) {
this.textureFile = handle;
this.useMipMaps = useMipMaps;
this.format = format;
this.minFilter = minFilter;
this.magFilter = magFilter;
this.uWrap = uWrap;
this.vWrap = vWrap;
}
}
public static class Region {
public Page page;
public int index;
public String name;
public float offsetX;
public float offsetY;
public int originalWidth;
public int originalHeight;
public boolean rotate;
public int left;
public int top;
public int width;
public int height;
public boolean flip;
public int[] splits;
public int[] pads;
}
final Array<Page> pages = new Array<Page>();
final Array<Region> regions = new Array<Region>();
public TextureAtlasData(FileHandle packFile, FileHandle imagesDir, boolean flip) {
BufferedReader reader = new BufferedReader(new InputStreamReader(packFile.read()), 64);
try {
Page pageImage = null;
while (true) {
String line = reader.readLine();
if (line == null)
break;
if (line.trim().length() == 0)
pageImage = null;
else if (pageImage == null) {
FileHandle file = imagesDir.child(line);
Format format = Format.valueOf(readValue(reader));
readTuple(reader);
TextureFilter min = TextureFilter.valueOf(tuple[0]);
TextureFilter max = TextureFilter.valueOf(tuple[1]);
String direction = readValue(reader);
TextureWrap repeatX = ClampToEdge;
TextureWrap repeatY = ClampToEdge;
if (direction.equals("x"))
repeatX = Repeat;
else if (direction.equals("y"))
repeatY = Repeat;
else if (direction.equals("xy")) {
repeatX = Repeat;
repeatY = Repeat;
}
pageImage = new Page(file, min.isMipMap(), format, min, max, repeatX, repeatY);
pages.add(pageImage);
} else {
boolean rotate = Boolean.valueOf(readValue(reader));
readTuple(reader);
int left = Integer.parseInt(tuple[0]);
int top = Integer.parseInt(tuple[1]);
readTuple(reader);
int width = Integer.parseInt(tuple[0]);
int height = Integer.parseInt(tuple[1]);
Region region = new Region();
region.page = pageImage;
region.left = left;
region.top = top;
region.width = width;
region.height = height;
region.name = line;
region.rotate = rotate;
if (readTuple(reader) == 4) { // split is optional
region.splits = new int[] { Integer.parseInt(tuple[0]), Integer.parseInt(tuple[1]),
Integer.parseInt(tuple[2]), Integer.parseInt(tuple[3]) };
if (readTuple(reader) == 4) { // pad is optional, but only present with splits
region.pads = new int[] { Integer.parseInt(tuple[0]), Integer.parseInt(tuple[1]),
Integer.parseInt(tuple[2]), Integer.parseInt(tuple[3]) };
readTuple(reader);
}
}
region.originalWidth = Integer.parseInt(tuple[0]);
region.originalHeight = Integer.parseInt(tuple[1]);
readTuple(reader);
region.offsetX = Integer.parseInt(tuple[0]);
region.offsetY = Integer.parseInt(tuple[1]);
region.index = Integer.parseInt(readValue(reader));
if (flip)
region.flip = true;
regions.add(region);
}
}
} catch (Exception ex) {
throw new GdxRuntimeException("Error reading pack file: " + packFile, ex);
} finally {
try {
reader.close();
} catch (IOException ignored) {
}
}
regions.sort(indexComparator);
}
public Array<Page> getPages() {
return pages;
}
public Array<Region> getRegions() {
return regions;
}
}
/** Creates an empty atlas to which regions can be added. */
public TextureAtlas() {
}
/**
* Loads the specified pack file using {@link FileType#Internal}, using the parent directory of the pack file to
* find the page images.
*/
public TextureAtlas(String internalPackFile) {
this(Gdx.files.internal(internalPackFile));
}
/** Loads the specified pack file, using the parent directory of the pack file to find the page images. */
public TextureAtlas(FileHandle packFile) {
this(packFile, packFile.parent());
}
/**
* @param flip
* If true, all regions loaded will be flipped for use with a perspective where 0,0 is the upper left
* corner.
* @see #TextureAtlas(FileHandle)
*/
public TextureAtlas(FileHandle packFile, boolean flip) {
this(packFile, packFile.parent(), flip);
}
public TextureAtlas(FileHandle packFile, FileHandle imagesDir) {
this(packFile, imagesDir, false);
}
/**
* @param flip
* If true, all regions loaded will be flipped for use with a perspective where 0,0 is the upper left
* corner.
*/
public TextureAtlas(FileHandle packFile, FileHandle imagesDir, boolean flip) {
this(new TextureAtlasData(packFile, imagesDir, flip));
}
public TextureAtlas(TextureAtlasData data) {
load(data);
}
private void load(TextureAtlasData data) {
ObjectMap<Page, Texture> pageToTexture = new ObjectMap<Page, Texture>();
for (Page page : data.pages) {
Texture texture = null;
if (page.texture == null) {
texture = new Texture(page.textureFile, page.format, page.useMipMaps);
texture.setFilter(page.minFilter, page.magFilter);
texture.setWrap(page.uWrap, page.vWrap);
} else {
texture = page.texture;
texture.setFilter(page.minFilter, page.magFilter);
texture.setWrap(page.uWrap, page.vWrap);
}
textures.add(texture);
pageToTexture.put(page, texture);
}
for (Region region : data.regions) {
int width = region.width;
int height = region.height;
AtlasRegion atlasRegion = new AtlasRegion(pageToTexture.get(region.page), region.left, region.top,
region.rotate ? height : width, region.rotate ? width : height);
atlasRegion.index = region.index;
atlasRegion.name = region.name;
atlasRegion.offsetX = region.offsetX;
atlasRegion.offsetY = region.offsetY;
atlasRegion.originalHeight = region.originalHeight;
atlasRegion.originalWidth = region.originalWidth;
atlasRegion.rotate = region.rotate;
atlasRegion.splits = region.splits;
atlasRegion.pads = region.pads;
if (region.flip)
atlasRegion.flip(false, true);
regions.add(atlasRegion);
}
}
/** Adds a region to the atlas. The specified texture will be disposed when the atlas is disposed. */
public AtlasRegion addRegion(String name, Texture texture, int x, int y, int width, int height) {
textures.add(texture);
AtlasRegion region = new AtlasRegion(texture, x, y, width, height);
region.name = name;
region.originalWidth = width;
region.originalHeight = height;
region.index = -1;
regions.add(region);
return region;
}
/** Adds a region to the atlas. The texture for the specified region will be disposed when the atlas is disposed. */
public AtlasRegion addRegion(String name, TextureRegion textureRegion) {
return addRegion(name, textureRegion.texture, textureRegion.getRegionX(), textureRegion.getRegionY(),
textureRegion.getRegionWidth(), textureRegion.getRegionHeight());
}
public AtlasRegion addRegion(AtlasRegion region) {
if (region == null)
return null;
regions.add(region);
return region;
}
/** Returns all regions in the atlas. */
public Array<AtlasRegion> getRegions() {
return regions;
}
/**
* Returns the first region found with the specified name. This method uses string comparison to find the region, so
* the result should be cached rather than calling this method multiple times.
*
* @return The region, or null.
*/
public AtlasRegion findRegion(String name) {
for (int i = 0, n = regions.size; i < n; i++)
if (regions.get(i).name.equals(name))
return regions.get(i);
return null;
}
/**
* Returns the first region found with the specified name and index. This method uses string comparison to find the
* region, so the result should be cached rather than calling this method multiple times.
*
* @return The region, or null.
*/
public AtlasRegion findRegion(String name, int index) {
for (int i = 0, n = regions.size; i < n; i++) {
AtlasRegion region = regions.get(i);
if (!region.name.equals(name))
continue;
if (region.index != index)
continue;
return region;
}
return null;
}
/**
* Returns all regions with the specified name, ordered by smallest to largest {@link AtlasRegion#index index}. This
* method uses string comparison to find the regions, so the result should be cached rather than calling this method
* multiple times.
*/
public Array<AtlasRegion> findRegions(String name) {
Array<AtlasRegion> matched = new Array();
for (int i = 0, n = regions.size; i < n; i++) {
AtlasRegion region = regions.get(i);
if (region.name.equals(name))
matched.add(new AtlasRegion(region));
}
return matched;
}
/**
* Returns all regions in the atlas as sprites. This method creates a new sprite for each region, so the result
* should be stored rather than calling this method multiple times.
*
* @see #createSprite(String)
*/
public Array<Sprite> createSprites() {
Array sprites = new Array(regions.size);
for (int i = 0, n = regions.size; i < n; i++)
sprites.add(newSprite(regions.get(i)));
return sprites;
}
/**
* Returns the first region found with the specified name as a sprite. If whitespace was stripped from the region
* when it was packed, the sprite is automatically positioned as if whitespace had not been stripped. This method
* uses string comparison to find the region and constructs a new sprite, so the result should be cached rather than
* calling this method multiple times.
*
* @return The sprite, or null.
*/
public Sprite createSprite(String name) {
for (int i = 0, n = regions.size; i < n; i++)
if (regions.get(i).name.equals(name))
return newSprite(regions.get(i));
return null;
}
/**
* Returns the first region found with the specified name and index as a sprite. This method uses string comparison
* to find the region and constructs a new sprite, so the result should be cached rather than calling this method
* multiple times.
*
* @return The sprite, or null.
* @see #createSprite(String)
*/
public Sprite createSprite(String name, int index) {
for (int i = 0, n = regions.size; i < n; i++) {
AtlasRegion region = regions.get(i);
if (!region.name.equals(name))
continue;
if (region.index != index)
continue;
return newSprite(regions.get(i));
}
return null;
}
/**
* Returns all regions with the specified name as sprites, ordered by smallest to largest {@link AtlasRegion#index
* index}. This method uses string comparison to find the regions and constructs new sprites, so the result should
* be cached rather than calling this method multiple times.
*
* @see #createSprite(String)
*/
public Array<Sprite> createSprites(String name) {
Array<Sprite> matched = new Array();
for (int i = 0, n = regions.size; i < n; i++) {
AtlasRegion region = regions.get(i);
if (region.name.equals(name))
matched.add(newSprite(region));
}
return matched;
}
private Sprite newSprite(AtlasRegion region) {
if (region.packedWidth == region.originalWidth && region.packedHeight == region.originalHeight) {
if (region.rotate) {
Sprite sprite = new Sprite(region);
sprite.setBounds(0, 0, region.getRegionHeight(), region.getRegionWidth());
sprite.rotate90(true);
return sprite;
}
return new Sprite(region);
}
return new AtlasSprite(region);
}
/**
* Returns the first region found with the specified name as a {@link NinePatch}. The region must have been packed
* with ninepatch splits. This method uses string comparison to find the region and constructs a new ninepatch, so
* the result should be cached rather than calling this method multiple times.
*
* @return The ninepatch, or null.
*/
public NinePatch createPatch(String name) {
for (int i = 0, n = regions.size; i < n; i++) {
AtlasRegion region = regions.get(i);
if (region.name.equals(name)) {
int[] splits = region.splits;
if (splits == null)
throw new IllegalArgumentException("Region does not have ninepatch splits: " + name);
NinePatch patch = new NinePatch(region, splits[0], splits[1], splits[2], splits[3]);
if (region.pads != null)
patch.setPadding(region.pads[0], region.pads[1], region.pads[2], region.pads[3]);
return patch;
}
}
return null;
}
/** @return the textures of the pages, unordered */
public Set<Texture> getTextures() {
return textures;
}
/**
* Releases all resources associated with this TextureAtlas instance. This releases all the textures backing all
* TextureRegions and Sprites, which should no longer be used after calling dispose.
*/
public void dispose() {
for (Texture texture : textures)
texture.dispose();
textures.clear();
}
static final Comparator<Region> indexComparator = new Comparator<Region>() {
public int compare(Region region1, Region region2) {
int i1 = region1.index;
if (i1 == -1)
i1 = Integer.MAX_VALUE;
int i2 = region2.index;
if (i2 == -1)
i2 = Integer.MAX_VALUE;
return i1 - i2;
}
};
static String readValue(BufferedReader reader) throws IOException {
String line = reader.readLine();
int colon = line.indexOf(':');
if (colon == -1)
throw new GdxRuntimeException("Invalid line: " + line);
return line.substring(colon + 1).trim();
}
/** Returns the number of tuple values read (2 or 4). */
static int readTuple(BufferedReader reader) throws IOException {
String line = reader.readLine();
int colon = line.indexOf(':');
if (colon == -1)
throw new GdxRuntimeException("Invalid line: " + line);
int i = 0, lastMatch = colon + 1;
for (i = 0; i < 3; i++) {
int comma = line.indexOf(',', lastMatch);
if (comma == -1) {
if (i == 0)
throw new GdxRuntimeException("Invalid line: " + line);
break;
}
tuple[i] = line.substring(lastMatch, comma).trim();
lastMatch = comma + 1;
}
tuple[i] = line.substring(lastMatch).trim();
return i + 1;
}
/** Describes the region of a packed image and provides information about the original image before it was packed. */
static public class AtlasRegion extends TextureRegion {
/**
* The number at the end of the original image file name, or -1 if none.<br>
* <br>
* When sprites are packed, if the original file name ends with a number, it is stored as the index and is not
* considered as part of the sprite's name. This is useful for keeping animation frames in order.
*
* @see TextureAtlas#findRegions(String)
*/
public int index;
/**
* The name of the original image file, up to the first underscore. Underscores denote special instructions to
* the texture packer.
*/
public String name;
/**
* The offset from the left of the original image to the left of the packed image, after whitespace was removed
* for packing.
*/
public float offsetX;
/**
* The offset from the bottom of the original image to the bottom of the packed image, after whitespace was
* removed for packing.
*/
public float offsetY;
/** The width of the image, after whitespace was removed for packing. */
public int packedWidth;
/** The height of the image, after whitespace was removed for packing. */
public int packedHeight;
/** The width of the image, before whitespace was removed and rotation was applied for packing. */
public int originalWidth;
/** The height of the image, before whitespace was removed for packing. */
public int originalHeight;
/** If true, the region has been rotated 90 degrees counter clockwise. */
public boolean rotate;
/** The ninepatch splits, or null if not a ninepatch. Has 4 elements: left, right, top, bottom. */
public int[] splits;
/**
* The ninepatch pads, or null if not a ninepatch or the has no padding. Has 4 elements: left, right, top,
* bottom.
*/
public int[] pads;
public AtlasRegion(Texture texture, int x, int y, int width, int height) {
super(texture, x, y, width, height);
packedWidth = width;
packedHeight = height;
}
public AtlasRegion(AtlasRegion region) {
setRegion(region);
index = region.index;
name = region.name;
offsetX = region.offsetX;
offsetY = region.offsetY;
packedWidth = region.packedWidth;
packedHeight = region.packedHeight;
originalWidth = region.originalWidth;
originalHeight = region.originalHeight;
rotate = region.rotate;
splits = region.splits;
}
/**
* Flips the region, adjusting the offset so the image appears to be flip as if no whitespace has been removed
* for packing.
*/
public void flip(boolean x, boolean y) {
super.flip(x, y);
if (x)
offsetX = originalWidth - offsetX - getRotatedPackedWidth();
if (y)
offsetY = originalHeight - offsetY - getRotatedPackedHeight();
}
/**
* Returns the packed width considering the rotate value, if it is true then it returns the packedHeight,
* otherwise it returns the packedWidth.
*/
public float getRotatedPackedWidth() {
return rotate ? packedHeight : packedWidth;
}
/**
* Returns the packed height considering the rotate value, if it is true then it returns the packedWidth,
* otherwise it returns the packedHeight.
*/
public float getRotatedPackedHeight() {
return rotate ? packedWidth : packedHeight;
}
}
/**
* A sprite that, if whitespace was stripped from the region when it was packed, is automatically positioned as if
* whitespace had not been stripped.
*/
static public class AtlasSprite extends Sprite {
final AtlasRegion region;
float originalOffsetX, originalOffsetY;
public AtlasSprite(AtlasRegion region) {
this.region = new AtlasRegion(region);
originalOffsetX = region.offsetX;
originalOffsetY = region.offsetY;
setRegion(region);
setOrigin(region.originalWidth / 2f, region.originalHeight / 2f);
int width = region.getRegionWidth();
int height = region.getRegionHeight();
if (region.rotate) {
super.rotate90(true);
super.setBounds(region.offsetX, region.offsetY, height, width);
} else
super.setBounds(region.offsetX, region.offsetY, width, height);
setColor(1, 1, 1, 1);
}
public AtlasSprite(AtlasSprite sprite) {
region = sprite.region;
this.originalOffsetX = sprite.originalOffsetX;
this.originalOffsetY = sprite.originalOffsetY;
set(sprite);
}
public void setPosition(float x, float y) {
super.setPosition(x + region.offsetX, y + region.offsetY);
}
public void setBounds(float x, float y, float width, float height) {
float widthRatio = width / region.originalWidth;
float heightRatio = height / region.originalHeight;
region.offsetX = originalOffsetX * widthRatio;
region.offsetY = originalOffsetY * heightRatio;
int packedWidth = region.rotate ? region.packedHeight : region.packedWidth;
int packedHeight = region.rotate ? region.packedWidth : region.packedHeight;
super.setBounds(x + region.offsetX, y + region.offsetY, packedWidth * widthRatio, packedHeight
* heightRatio);
}
public void setSize(float width, float height) {
setBounds(getX(), getY(), width, height);
}
public void setOrigin(float originX, float originY) {
super.setOrigin(originX - region.offsetX, originY - region.offsetY);
}
public void flip(boolean x, boolean y) {
// Flip texture.
if (flipx == x && flipy == y)
return;
super.flip(x, y);
float oldOriginX = getOriginX();
float oldOriginY = getOriginY();
float oldOffsetX = region.offsetX;
float oldOffsetY = region.offsetY;
float widthRatio = getWidthRatio();
float heightRatio = getHeightRatio();
region.offsetX = originalOffsetX;
region.offsetY = originalOffsetY;
region.flip(x, y); // Updates x and y offsets.
originalOffsetX = region.offsetX;
originalOffsetY = region.offsetY;
region.offsetX *= widthRatio;
region.offsetY *= heightRatio;
// Update position and origin with new offsets.
translate(region.offsetX - oldOffsetX, region.offsetY - oldOffsetY);
setOrigin(oldOriginX, oldOriginY);
}
public void rotate90(boolean clockwise) {
// Rotate texture.
super.rotate90(clockwise);
float oldOriginX = getOriginX();
float oldOriginY = getOriginY();
float oldOffsetX = region.offsetX;
float oldOffsetY = region.offsetY;
float widthRatio = getWidthRatio();
float heightRatio = getHeightRatio();
if (clockwise) {
region.offsetX = oldOffsetY;
region.offsetY = region.originalHeight * heightRatio - oldOffsetX - region.packedWidth * widthRatio;
} else {
region.offsetX = region.originalWidth * widthRatio - oldOffsetY - region.packedHeight * heightRatio;
region.offsetY = oldOffsetX;
}
// Update position and origin with new offsets.
translate(region.offsetX - oldOffsetX, region.offsetY - oldOffsetY);
setOrigin(oldOriginX, oldOriginY);
}
public float getX() {
return super.getX() - region.offsetX;
}
public float getY() {
return super.getY() - region.offsetY;
}
public float getOriginX() {
return super.getOriginX() + region.offsetX;
}
public float getOriginY() {
return super.getOriginY() + region.offsetY;
}
public float getWidth() {
return super.getWidth() / region.getRotatedPackedWidth() * region.originalWidth;
}
public float getHeight() {
return super.getHeight() / region.getRotatedPackedHeight() * region.originalHeight;
}
public float getWidthRatio() {
return super.getWidth() / region.getRotatedPackedWidth();
}
public float getHeightRatio() {
return super.getHeight() / region.getRotatedPackedHeight();
}
public AtlasRegion getAtlasRegion() {
return region;
}
}
}