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