• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
Node(int x, int y, int width, int height, Node leftChild, Node rightChild, String leaveName)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 
Node()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 */
ImagePacker(int width, int height, int padding, boolean duplicateBorder)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 */
insertImage(String name, BufferedImage image)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 
insert(Node node, Rectangle rect)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 */
getImage()209 	public BufferedImage getImage () {
210 		return image;
211 	}
212 
213 	/** @return the rectangle in the output image of each inserted image */
getRects()214 	public Map<String, Rectangle> getRects () {
215 		return rects;
216 	}
217 
main(String[] argv)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 
createImage(int width, int height, Color color)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