/*******************************************************************************
* 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 com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Blending;
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.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.PixmapTextureData;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap.Keys;
import com.badlogic.gdx.utils.OrderedMap;
/**
* Packs {@link Pixmap} instances into one more more {@link Page} instances to generate
* an atlas of Pixmap instances. Provides means to directly convert the pixmap atlas to a {@link TextureAtlas}. The
* packer supports padding and border pixel duplication, specified during construction. The packer supports incremental inserts
* and updates of TextureAtlases generated with this class.</p>
*
* All methods except {@link #getPage(String)} and {@link #getPages()} are thread safe. The methods {@link #generateTextureAtlas(TextureFilter, TextureFilter)}
* and {@link #updateTextureAtlas(TextureAtlas, TextureFilter, TextureFilter)} need to be called on the rendering thread, all
* other methods can be called from any thread.</p>
*
* One-off usage:
* <pre>
* // 512x512 pixel pages, RGB565 format, 2 pixels of padding, border duplication
* PixmapPacker packer = new PixmapPacker(512, 512, Format.RGB565, 2, true);
* packer.pack("First Pixmap", pixmap1);
* packer.pack("Second Pixmap", pixmap2);
* TextureAtlas altas = packer.generateTextureAtlas(TextureFilter.Nearest, TextureFilter.Nearest);
* </pre>
*
* Note that you should not dispose the packer in this usage pattern. Instead, dispose the TextureAtlas
* if no longer needed.
*
* Incremental usage:
* <pre>
* // 512x512 pixel pages, RGB565 format, 2 pixels of padding, no border duplication
* PixmapPacker packer = new PixmapPacker(512, 512, Format.RGB565, 2, false);
* TextureAtlas incrementalAtlas = new TextureAtlas();
*
* // potentially on a separate thread, e.g. downloading thumbnails
* packer.pack("thumbnail", thumbnail);
*
* // on the rendering thread, every frame
* packer.updateTextureAtlas(incrementalAtlas, TextureFilter.Linear, TextureFilter.Linear);
*
* // once the atlas is no longer needed, make sure you get the final additions. This might
* // be more elaborate depending on your threading model.
* packer.updateTextureAtlas(incrementalAtlas, TextureFilter.Linear, TextureFilter.Linear);
* incrementalAtlas.dispose();
* </pre>
*
* Pixmap-only usage:
* <pre>
* PixmapPacker packer = new PixmapPacker(512, 512, Format.RGB565, 2, true);
* packer.pack("First Pixmap", pixmap1);
* packer.pack("Second Pixmap", pixmap2);
*
* // do something interesting with the resulting pages
* for(Page page: packer.getPages()) {
* }
*
* // dispose of the packer in this case
* packer.dispose();
* </pre>
*/
public class PixmapPacker implements Disposable {
static final class Node {
public Node leftChild;
public Node rightChild;
public Rectangle rect;
public String leaveName;
public Node (int x, int y, int width, int height, Node leftChild, Node rightChild, String leaveName) {
this.rect = new Rectangle(x, y, width, height);
this.leftChild = leftChild;
this.rightChild = rightChild;
this.leaveName = leaveName;
}
public Node () {
rect = new Rectangle();
}
}
public class Page {
Node root;
OrderedMap<String, Rectangle> rects;
Pixmap image;
Texture texture;
Array<String> addedRects = new Array<String>();
public Pixmap getPixmap() {
return image;
}
}
final int pageWidth;
final int pageHeight;
final Format pageFormat;
final int padding;
final boolean duplicateBorder;
final Array<Page> pages = new Array<Page>();
Page currPage;
boolean disposed;
/** <p>
* Creates a new ImagePacker which will insert all supplied images into a <code>width</code> by <code>height</code> image.
* <code>padding</code> specifies the minimum number of pixels to insert between images. <code>border</code> will duplicate the
* border pixels of the inserted images to avoid seams when rendering with bi-linear filtering on.
* </p>
*
* @param width the width of the output image
* @param height the height of the output image
* @param padding the number of padding pixels
* @param duplicateBorder whether to duplicate the border */
public PixmapPacker (int width, int height, Format format, int padding, boolean duplicateBorder) {
this.pageWidth = width;
this.pageHeight = height;
this.pageFormat = format;
this.padding = padding;
this.duplicateBorder = duplicateBorder;
newPage();
}
/** <p>
* Inserts the given {@link Pixmap}. You can later on retrieve the images position in the output image via the supplied name and the
* method {@link #getRect(String)}.
* </p>
*
* @param name the name of the image
* @param image the image
* @return Rectangle describing the area the pixmap was rendered to or null.
* @throws RuntimeException in case the image did not fit due to the page size being to small or providing a duplicate name */
public synchronized Rectangle pack (String name, Pixmap image) {
if(disposed) return null;
if (getRect(name) != null) throw new RuntimeException("Key with name '" + name + "' is already in map");
int borderPixels = padding + (duplicateBorder ? 1 : 0);
borderPixels <<= 1;
if(image.getWidth() >= pageWidth + borderPixels|| image.getHeight() >= pageHeight + borderPixels) throw new GdxRuntimeException("page size for '" + name + "' to small");
Rectangle rect = new Rectangle(0, 0, image.getWidth() + borderPixels, image.getHeight() + borderPixels);
Node node = insert(currPage.root, rect);
if (node == null) {
newPage();
return pack(name, image);
}
node.leaveName = name;
rect = new Rectangle(node.rect);
rect.width -= borderPixels;
rect.height -= borderPixels;
borderPixels >>= 1;
rect.x += borderPixels;
rect.y += borderPixels;
currPage.rects.put(name, rect);
Blending blending = Pixmap.getBlending();
Pixmap.setBlending(Blending.None);
this.currPage.image.drawPixmap(image, (int)rect.x, (int)rect.y);
Pixmap.setBlending(blending);
// not terribly efficient (as the rest of the code) but will do :p
if (duplicateBorder) {
this.currPage.image.drawPixmap(image, (int)rect.x, (int)rect.y - 1, (int)rect.x + (int)rect.width, (int)rect.y, 0, 0, image.getWidth(), 1);
this.currPage.image.drawPixmap(image, (int)rect.x, (int)rect.y + (int)rect.height, (int)rect.x + (int)rect.width, (int)rect.y + (int)rect.height + 1, 0,
image.getHeight() - 1, image.getWidth(), image.getHeight());
this.currPage.image.drawPixmap(image, (int)rect.x - 1, (int)rect.y, (int)rect.x, (int)rect.y + (int)rect.height, 0, 0, 1, image.getHeight());
this.currPage.image.drawPixmap(image, (int)rect.x + (int)rect.width, (int)rect.y, (int)rect.x + (int)rect.width + 1, (int)rect.y + (int)rect.height, image.getWidth() - 1, 0,
image.getWidth(), image.getHeight());
this.currPage.image.drawPixmap(image, (int)rect.x - 1, (int)rect.y - 1, (int)rect.x, (int)rect.y, 0, 0, 1, 1);
this.currPage.image.drawPixmap(image, (int)rect.x + (int)rect.width, (int)rect.y - 1, (int)rect.x + (int)rect.width + 1, (int)rect.y, image.getWidth() - 1, 0,
image.getWidth(), 1);
this.currPage.image.drawPixmap(image, (int)rect.x - 1, (int)rect.y + (int)rect.height, (int)rect.x, (int)rect.y + (int)rect.height + 1, 0, image.getHeight() - 1, 1,
image.getHeight());
this.currPage.image.drawPixmap(image, (int)rect.x + (int)rect.width, (int)rect.y + (int)rect.height, (int)rect.x + (int)rect.width + 1, (int)rect.y + (int)rect.height + 1,
image.getWidth() - 1, image.getHeight() - 1, image.getWidth(), image.getHeight());
}
currPage.addedRects.add(name);
return rect;
}
private void newPage() {
Page page = new Page();
page.image = new Pixmap(pageWidth, pageHeight, pageFormat);
page.root = new Node(0, 0, pageWidth, pageHeight, null, null, null);
page.rects = new OrderedMap<String, Rectangle>();
pages.add(page);
currPage = page;
}
private Node insert (Node node, Rectangle rect) {
if (node.leaveName == null && node.leftChild != null && node.rightChild != null) {
Node newNode = null;
newNode = insert(node.leftChild, rect);
if (newNode == null) newNode = insert(node.rightChild, rect);
return newNode;
} else {
if (node.leaveName != null) return null;
if (node.rect.width == rect.width && node.rect.height == rect.height) return node;
if (node.rect.width < rect.width || node.rect.height < rect.height) return null;
node.leftChild = new Node();
node.rightChild = new Node();
int deltaWidth = (int)node.rect.width - (int)rect.width;
int deltaHeight = (int)node.rect.height - (int)rect.height;
if (deltaWidth > deltaHeight) {
node.leftChild.rect.x = node.rect.x;
node.leftChild.rect.y = node.rect.y;
node.leftChild.rect.width = rect.width;
node.leftChild.rect.height = node.rect.height;
node.rightChild.rect.x = node.rect.x + rect.width;
node.rightChild.rect.y = node.rect.y;
node.rightChild.rect.width = node.rect.width - rect.width;
node.rightChild.rect.height = node.rect.height;
} else {
node.leftChild.rect.x = node.rect.x;
node.leftChild.rect.y = node.rect.y;
node.leftChild.rect.width = node.rect.width;
node.leftChild.rect.height = rect.height;
node.rightChild.rect.x = node.rect.x;
node.rightChild.rect.y = node.rect.y + rect.height;
node.rightChild.rect.width = node.rect.width;
node.rightChild.rect.height = node.rect.height - rect.height;
}
return insert(node.leftChild, rect);
}
}
/** @return the {@link Page} instances created so far. This method is not thread safe! */
public Array<Page> getPages () {
return pages;
}
/**
* @param name the name of the image
* @return the rectangle for the image in the page it's stored in or null
*/
public synchronized Rectangle getRect(String name) {
for(Page page: pages) {
Rectangle rect = page.rects.get(name);
if(rect != null) return rect;
}
return null;
}
/**
* @param name the name of the image
* @return the page the image is stored in or null
*/
public synchronized Page getPage(String name) {
for(Page page: pages) {
Rectangle rect = page.rects.get(name);
if(rect != null) return page;
}
return null;
}
/**
* Disposes all resources, including Pixmap instances for the pages
* created so far. These page Pixmap instances are shared with
* any {@link TextureAtlas} generated or updated by either {@link #generateTextureAtlas(TextureFilter, TextureFilter)}
* or {@link #updateTextureAtlas(TextureAtlas, TextureFilter, TextureFilter)}. Do
* not call this method if you generated or updated a TextureAtlas, instead
* dispose the TextureAtlas.
*/
public synchronized void dispose() {
for(Page page: pages) {
page.image.dispose();
}
disposed = true;
}
/**
* Generates a new {@link TextureAtlas} from the {@link Pixmap} instances inserted so far.
* @param minFilter
* @param magFilter
* @return the TextureAtlas
*/
public synchronized TextureAtlas generateTextureAtlas (TextureFilter minFilter, TextureFilter magFilter) {
TextureAtlas atlas = new TextureAtlas();
for(Page page: pages) {
if(page.rects.size != 0) {
Texture texture = new Texture(new ManagedPixmapTextureData(page.image, page.image.getFormat(), true)) {
@Override
public void dispose () {
super.dispose();
}
};
texture.setFilter(minFilter, magFilter);
Keys<String> names = page.rects.keys();
for(String name: names) {
Rectangle rect = page.rects.get(name);
TextureRegion region = new TextureRegion(texture, (int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
atlas.addRegion(name, region);
}
}
}
return atlas;
}
/**
* Updates the given {@link TextureAtlas}, adding any new {@link Pixmap} instances packed since the last
* call to this method. This can be used to insert Pixmap instances on a separate thread via {@link #pack(String, Pixmap)}
* and update the TextureAtlas on the rendering thread. This method must be called on the rendering thread.
*/
public synchronized void updateTextureAtlas(TextureAtlas atlas, TextureFilter minFilter, TextureFilter magFilter) {
for(Page page: pages) {
if(page.texture == null) {
if(page.rects.size != 0 && page.addedRects.size > 0) {
page.texture = new Texture(new ManagedPixmapTextureData(page.image, page.image.getFormat(), false)) {
@Override
public void dispose () {
super.dispose();
}
};
page.texture.setFilter(minFilter, magFilter);
for(String name: page.addedRects) {
Rectangle rect = page.rects.get(name);
TextureRegion region = new TextureRegion(page.texture, (int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
atlas.addRegion(name, region);
}
page.addedRects.clear();
}
} else {
if(page.addedRects.size > 0) {
page.texture.load(page.texture.getTextureData());
for(String name: page.addedRects) {
Rectangle rect = page.rects.get(name);
TextureRegion region = new TextureRegion(page.texture, (int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
atlas.addRegion(name, region);
}
page.addedRects.clear();
return;
}
}
}
}
public int getPageWidth () {
return pageWidth;
}
public int getPageHeight () {
return pageHeight;
}
public int getPadding() {
return padding;
}
public boolean duplicateBoarder() {
return duplicateBorder;
}
public class ManagedPixmapTextureData extends PixmapTextureData {
public ManagedPixmapTextureData (Pixmap pixmap, Format format, boolean useMipMaps) {
super(pixmap, format, useMipMaps, false);
}
@Override
public boolean isManaged () {
return true;
}
}
}