Home | History | Annotate | Download | only in imagepacker
      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.imagepacker;
     18 
     19 import java.awt.Color;
     20 import java.awt.Graphics2D;
     21 import java.awt.Rectangle;
     22 import java.awt.image.BufferedImage;
     23 import java.io.File;
     24 import java.io.IOException;
     25 import java.util.Arrays;
     26 import java.util.Comparator;
     27 import java.util.HashMap;
     28 import java.util.Map;
     29 import java.util.Random;
     30 
     31 import javax.imageio.ImageIO;
     32 
     33 /** <p>
     34  * A simple image packer class based on the nice algorithm by blackpawn.
     35  * </p>
     36  *
     37  * <p>
     38  * See http://www.blackpawn.com/texts/lightmaps/default.html for details.
     39  * </p>
     40  *
     41  * <p>
     42  * <b>Usage:</b> instanciate an <code>ImagePacker</code> instance, load and optionally sort the images you want to add by size
     43  * (e.g. area) then insert each image via a call to {@link #insertImage(String, BufferedImage)}. When you are done with inserting
     44  * images you can call {@link #getImage()} for the {@link BufferedImage} that holds the packed images. Additionally you can get a
     45  * <code>Map<String, Rectangle></code> where the keys the names you specified when inserting and the values are the rectangles
     46  * within the packed image where that specific image is located. All things are given in pixels.
     47  * </p>
     48  *
     49  * <p>
     50  * See the {@link #main(String[])} method for an example that will generate 100 random images, pack them and then output the
     51  * packed image as a png along with a json file holding the image descriptors.
     52  * </p>
     53  *
     54  * <p>
     55  * In some cases it is beneficial to add padding and to duplicate the border pixels of an inserted image so that there is no
     56  * bleeding of neighbouring pixels when using the packed image as a texture. You can specify the padding as well as whether to
     57  * duplicate the border pixels in the constructor.
     58  * </p>
     59  *
     60  * <p>
     61  * Happy packing!
     62  * </p>
     63  *
     64  * @author mzechner */
     65 public class ImagePacker {
     66 	static final class Node {
     67 		public Node leftChild;
     68 		public Node rightChild;
     69 		public Rectangle rect;
     70 		public String leaveName;
     71 
     72 		public Node (int x, int y, int width, int height, Node leftChild, Node rightChild, String leaveName) {
     73 			this.rect = new Rectangle(x, y, width, height);
     74 			this.leftChild = leftChild;
     75 			this.rightChild = rightChild;
     76 			this.leaveName = leaveName;
     77 		}
     78 
     79 		public Node () {
     80 			rect = new Rectangle();
     81 		}
     82 	}
     83 
     84 	BufferedImage image;
     85 	int padding;
     86 	boolean duplicateBorder;
     87 	Node root;
     88 	Map<String, Rectangle> rects;
     89 
     90 	/** <p>
     91 	 * Creates a new ImagePacker which will insert all supplied images into a <code>width</code> by <code>height</code> image.
     92 	 * <code>padding</code> specifies the minimum number of pixels to insert between images. <code>border</code> will duplicate the
     93 	 * border pixels of the inserted images to avoid seams when rendering with bi-linear filtering on.
     94 	 * </p>
     95 	 *
     96 	 * @param width the width of the output image
     97 	 * @param height the height of the output image
     98 	 * @param padding the number of padding pixels
     99 	 * @param duplicateBorder whether to duplicate the border */
    100 	public ImagePacker (int width, int height, int padding, boolean duplicateBorder) {
    101 		this.image = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    102 		this.padding = padding;
    103 		this.duplicateBorder = duplicateBorder;
    104 		this.root = new Node(0, 0, width, height, null, null, null);
    105 		this.rects = new HashMap<String, Rectangle>();
    106 	}
    107 
    108 	/** <p>
    109 	 * Inserts the given image. You can later on retrieve the images position in the output image via the supplied name and the
    110 	 * method {@link #getRects()}.
    111 	 * </p>
    112 	 *
    113 	 * @param name the name of the image
    114 	 * @param image the image
    115 	 * @throws RuntimeException in case the image did not fit or you specified a duplicate name */
    116 	public void insertImage (String name, BufferedImage image) {
    117 		if (rects.containsKey(name)) throw new RuntimeException("Key with name '" + name + "' is already in map");
    118 
    119 		int borderPixels = padding + (duplicateBorder ? 1 : 0);
    120 		borderPixels <<= 1;
    121 		Rectangle rect = new Rectangle(0, 0, image.getWidth() + borderPixels, image.getHeight() + borderPixels);
    122 		Node node = insert(root, rect);
    123 
    124 		if (node == null) throw new RuntimeException("Image didn't fit");
    125 
    126 		node.leaveName = name;
    127 		rect = new Rectangle(node.rect);
    128 		rect.width -= borderPixels;
    129 		rect.height -= borderPixels;
    130 		borderPixels >>= 1;
    131 		rect.x += borderPixels;
    132 		rect.y += borderPixels;
    133 		rects.put(name, rect);
    134 
    135 		Graphics2D g = this.image.createGraphics();
    136 		g.drawImage(image, rect.x, rect.y, null);
    137 
    138 		// not terribly efficient (as the rest of the code) but will do :p
    139 		if (duplicateBorder) {
    140 			g.drawImage(image, rect.x, rect.y - 1, rect.x + rect.width, rect.y, 0, 0, image.getWidth(), 1, null);
    141 			g.drawImage(image, rect.x, rect.y + rect.height, rect.x + rect.width, rect.y + rect.height + 1, 0,
    142 				image.getHeight() - 1, image.getWidth(), image.getHeight(), null);
    143 
    144 			g.drawImage(image, rect.x - 1, rect.y, rect.x, rect.y + rect.height, 0, 0, 1, image.getHeight(), null);
    145 			g.drawImage(image, rect.x + rect.width, rect.y, rect.x + rect.width + 1, rect.y + rect.height, image.getWidth() - 1, 0,
    146 				image.getWidth(), image.getHeight(), null);
    147 
    148 			g.drawImage(image, rect.x - 1, rect.y - 1, rect.x, rect.y, 0, 0, 1, 1, null);
    149 			g.drawImage(image, rect.x + rect.width, rect.y - 1, rect.x + rect.width + 1, rect.y, image.getWidth() - 1, 0,
    150 				image.getWidth(), 1, null);
    151 
    152 			g.drawImage(image, rect.x - 1, rect.y + rect.height, rect.x, rect.y + rect.height + 1, 0, image.getHeight() - 1, 1,
    153 				image.getHeight(), null);
    154 			g.drawImage(image, rect.x + rect.width, rect.y + rect.height, rect.x + rect.width + 1, rect.y + rect.height + 1,
    155 				image.getWidth() - 1, image.getHeight() - 1, image.getWidth(), image.getHeight(), null);
    156 		}
    157 
    158 		g.dispose();
    159 	}
    160 
    161 	private Node insert (Node node, Rectangle rect) {
    162 		if (node.leaveName == null && node.leftChild != null && node.rightChild != null) {
    163 			Node newNode = null;
    164 
    165 			newNode = insert(node.leftChild, rect);
    166 			if (newNode == null) newNode = insert(node.rightChild, rect);
    167 
    168 			return newNode;
    169 		} else {
    170 			if (node.leaveName != null) return null;
    171 
    172 			if (node.rect.width == rect.width && node.rect.height == rect.height) return node;
    173 
    174 			if (node.rect.width < rect.width || node.rect.height < rect.height) return null;
    175 
    176 			node.leftChild = new Node();
    177 			node.rightChild = new Node();
    178 
    179 			int deltaWidth = node.rect.width - rect.width;
    180 			int deltaHeight = node.rect.height - rect.height;
    181 
    182 			if (deltaWidth > deltaHeight) {
    183 				node.leftChild.rect.x = node.rect.x;
    184 				node.leftChild.rect.y = node.rect.y;
    185 				node.leftChild.rect.width = rect.width;
    186 				node.leftChild.rect.height = node.rect.height;
    187 
    188 				node.rightChild.rect.x = node.rect.x + rect.width;
    189 				node.rightChild.rect.y = node.rect.y;
    190 				node.rightChild.rect.width = node.rect.width - rect.width;
    191 				node.rightChild.rect.height = node.rect.height;
    192 			} else {
    193 				node.leftChild.rect.x = node.rect.x;
    194 				node.leftChild.rect.y = node.rect.y;
    195 				node.leftChild.rect.width = node.rect.width;
    196 				node.leftChild.rect.height = rect.height;
    197 
    198 				node.rightChild.rect.x = node.rect.x;
    199 				node.rightChild.rect.y = node.rect.y + rect.height;
    200 				node.rightChild.rect.width = node.rect.width;
    201 				node.rightChild.rect.height = node.rect.height - rect.height;
    202 			}
    203 
    204 			return insert(node.leftChild, rect);
    205 		}
    206 	}
    207 
    208 	/** @return the output image */
    209 	public BufferedImage getImage () {
    210 		return image;
    211 	}
    212 
    213 	/** @return the rectangle in the output image of each inserted image */
    214 	public Map<String, Rectangle> getRects () {
    215 		return rects;
    216 	}
    217 
    218 	public static void main (String[] argv) throws IOException {
    219 		Random rand = new Random(0);
    220 		ImagePacker packer = new ImagePacker(512, 512, 1, true);
    221 
    222 		BufferedImage[] images = new BufferedImage[100];
    223 		for (int i = 0; i < images.length; i++) {
    224 			Color color = new Color((float)Math.random(), (float)Math.random(), (float)Math.random(), 1);
    225 			images[i] = createImage(rand.nextInt(50) + 10, rand.nextInt(50) + 10, color);
    226 		}
    227 // BufferedImage[] images = { ImageIO.read( new File( "test.png" ) ) };
    228 
    229 		Arrays.sort(images, new Comparator<BufferedImage>() {
    230 			@Override
    231 			public int compare (BufferedImage o1, BufferedImage o2) {
    232 				return o2.getWidth() * o2.getHeight() - o1.getWidth() * o1.getHeight();
    233 			}
    234 		});
    235 
    236 		for (int i = 0; i < images.length; i++)
    237 			packer.insertImage("" + i, images[i]);
    238 
    239 		ImageIO.write(packer.getImage(), "png", new File("packed.png"));
    240 	}
    241 
    242 	private static BufferedImage createImage (int width, int height, Color color) {
    243 		BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    244 		Graphics2D g = image.createGraphics();
    245 		g.setColor(color);
    246 		g.fillRect(0, 0, width, height);
    247 		g.dispose();
    248 		return image;
    249 	}
    250 }
    251