com.badlogic.gdx.graphics.g2d.TextureAtlas.java Source code

Java tutorial

Introduction

Here is the source code for com.badlogic.gdx.graphics.g2d.TextureAtlas.java

Source

/*******************************************************************************
 * 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.*;

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;
import com.badlogic.gdx.utils.ObjectSet;
import com.badlogic.gdx.utils.Sort;
import com.badlogic.gdx.utils.StreamUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;

/** 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 ObjectSet<Texture> textures = new ObjectSet(4);
    private final Array<AtlasRegion> regions = new Array();

    public static class TextureAtlasData {
        public static class Page {
            public final FileHandle textureFile;
            public Texture texture;
            public final float width, height;
            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, float width, float height, boolean useMipMaps, Format format,
                    TextureFilter minFilter, TextureFilter magFilter, TextureWrap uWrap, TextureWrap vWrap) {
                this.width = width;
                this.height = height;
                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();
        final Array<Region> regions = new Array();

        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);

                        float width = 0, height = 0;
                        if (readTuple(reader) == 2) { // size is only optional for an atlas packed with an old TexturePacker.
                            width = Integer.parseInt(tuple[0]);
                            height = Integer.parseInt(tuple[1]);
                            readTuple(reader);
                        }
                        Format format = Format.valueOf(tuple[0]);

                        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, width, height, 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 {
                StreamUtils.closeQuietly(reader);
            }

            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));
    }

    /** @param data May be null. */
    public TextureAtlas(TextureAtlasData data) {
        if (data != null)
            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());
    }

    /** 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 ObjectSet<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 (1, 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)
                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);
            originalWidth = width;
            originalHeight = 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;
        }

        @Override
        /** 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;
        }

        public String toString() {
            return name;
        }
    }

    /** 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);
        }

        @Override
        public void setPosition(float x, float y) {
            super.setPosition(x + region.offsetX, y + region.offsetY);
        }

        @Override
        public void setX(float x) {
            super.setX(x + region.offsetX);
        }

        @Override
        public void setY(float y) {
            super.setY(y + region.offsetY);
        }

        @Override
        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);
        }

        @Override
        public void setSize(float width, float height) {
            setBounds(getX(), getY(), width, height);
        }

        @Override
        public void setOrigin(float originX, float originY) {
            super.setOrigin(originX - region.offsetX, originY - region.offsetY);
        }

        @Override
        public void setOriginCenter() {
            super.setOrigin(width / 2 - region.offsetX, height / 2 - region.offsetY);
        }

        @Override
        public void flip(boolean x, boolean y) {
            // Flip texture.
            if (region.rotate)
                super.flip(y, x);
            else
                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);
        }

        @Override
        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);
        }

        @Override
        public float getX() {
            return super.getX() - region.offsetX;
        }

        @Override
        public float getY() {
            return super.getY() - region.offsetY;
        }

        @Override
        public float getOriginX() {
            return super.getOriginX() + region.offsetX;
        }

        @Override
        public float getOriginY() {
            return super.getOriginY() + region.offsetY;
        }

        @Override
        public float getWidth() {
            return super.getWidth() / region.getRotatedPackedWidth() * region.originalWidth;
        }

        @Override
        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;
        }

        public String toString() {
            return region.toString();
        }
    }
}