Home | History | Annotate | Download | only in texturepacker
      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.texturepacker;
     18 
     19 import com.badlogic.gdx.tools.texturepacker.TexturePacker.Alias;
     20 import com.badlogic.gdx.tools.texturepacker.TexturePacker.Rect;
     21 import com.badlogic.gdx.tools.texturepacker.TexturePacker.Settings;
     22 import com.badlogic.gdx.utils.Array;
     23 
     24 import java.awt.Graphics2D;
     25 import java.awt.Image;
     26 import java.awt.RenderingHints;
     27 import java.awt.image.BufferedImage;
     28 import java.awt.image.WritableRaster;
     29 import java.io.File;
     30 import java.io.IOException;
     31 import java.math.BigInteger;
     32 import java.security.MessageDigest;
     33 import java.security.NoSuchAlgorithmException;
     34 import java.util.Arrays;
     35 import java.util.HashMap;
     36 import java.util.regex.Matcher;
     37 import java.util.regex.Pattern;
     38 
     39 import javax.imageio.ImageIO;
     40 
     41 public class ImageProcessor {
     42 	static private final BufferedImage emptyImage = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR);
     43 	static private Pattern indexPattern = Pattern.compile("(.+)_(\\d+)$");
     44 
     45 	private String rootPath;
     46 	private final Settings settings;
     47 	private final HashMap<String, Rect> crcs = new HashMap();
     48 	private final Array<Rect> rects = new Array();
     49 	private float scale = 1;
     50 
     51 	/** @param rootDir Used to strip the root directory prefix from image file names, can be null. */
     52 	public ImageProcessor (File rootDir, Settings settings) {
     53 		this.settings = settings;
     54 
     55 		if (rootDir != null) {
     56 			rootPath = rootDir.getAbsolutePath().replace('\\', '/');
     57 			if (!rootPath.endsWith("/")) rootPath += "/";
     58 		}
     59 	}
     60 
     61 	public ImageProcessor (Settings settings) {
     62 		this(null, settings);
     63 	}
     64 
     65 	/** The image won't be kept in-memory during packing if {@link Settings#limitMemory} is true. */
     66 	public void addImage (File file) {
     67 		BufferedImage image;
     68 		try {
     69 			image = ImageIO.read(file);
     70 		} catch (IOException ex) {
     71 			throw new RuntimeException("Error reading image: " + file, ex);
     72 		}
     73 		if (image == null) throw new RuntimeException("Unable to read image: " + file);
     74 
     75 		String name = file.getAbsolutePath().replace('\\', '/');
     76 
     77 		// Strip root dir off front of image path.
     78 		if (rootPath != null) {
     79 			if (!name.startsWith(rootPath)) throw new RuntimeException("Path '" + name + "' does not start with root: " + rootPath);
     80 			name = name.substring(rootPath.length());
     81 		}
     82 
     83 		// Strip extension.
     84 		int dotIndex = name.lastIndexOf('.');
     85 		if (dotIndex != -1) name = name.substring(0, dotIndex);
     86 
     87 		Rect rect = addImage(image, name);
     88 		if (rect != null && settings.limitMemory) rect.unloadImage(file);
     89 	}
     90 
     91 	/** The image will be kept in-memory during packing.
     92 	 * @see #addImage(File) */
     93 	public Rect addImage (BufferedImage image, String name) {
     94 		Rect rect = processImage(image, name);
     95 
     96 		if (rect == null) {
     97 			if(!settings.silent) System.out.println("Ignoring blank input image: " + name);
     98 			return null;
     99 		}
    100 
    101 		if (settings.alias) {
    102 			String crc = hash(rect.getImage(this));
    103 			Rect existing = crcs.get(crc);
    104 			if (existing != null) {
    105 				if (!settings.silent) System.out.println(rect.name + " (alias of " + existing.name + ")");
    106 				existing.aliases.add(new Alias(rect));
    107 				return null;
    108 			}
    109 			crcs.put(crc, rect);
    110 		}
    111 
    112 		rects.add(rect);
    113 		return rect;
    114 	}
    115 
    116 	public void setScale (float scale) {
    117 		this.scale = scale;
    118 	}
    119 
    120 	public Array<Rect> getImages () {
    121 		return rects;
    122 	}
    123 
    124 	public void clear () {
    125 		rects.clear();
    126 		crcs.clear();
    127 	}
    128 
    129 	/** Returns a rect for the image describing the texture region to be packed, or null if the image should not be packed. */
    130 	Rect processImage (BufferedImage image, String name) {
    131 		if (scale <= 0) throw new IllegalArgumentException("scale cannot be <= 0: " + scale);
    132 
    133 		int width = image.getWidth(), height = image.getHeight();
    134 
    135 		if (image.getType() != BufferedImage.TYPE_4BYTE_ABGR) {
    136 			BufferedImage newImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    137 			newImage.getGraphics().drawImage(image, 0, 0, null);
    138 			image = newImage;
    139 		}
    140 
    141 		boolean isPatch = name.endsWith(".9");
    142 		int[] splits = null, pads = null;
    143 		Rect rect = null;
    144 		if (isPatch) {
    145 			// Strip ".9" from file name, read ninepatch split pixels, and strip ninepatch split pixels.
    146 			name = name.substring(0, name.length() - 2);
    147 			splits = getSplits(image, name);
    148 			pads = getPads(image, name, splits);
    149 			// Strip split pixels.
    150 			width -= 2;
    151 			height -= 2;
    152 			BufferedImage newImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    153 			newImage.getGraphics().drawImage(image, 0, 0, width, height, 1, 1, width + 1, height + 1, null);
    154 			image = newImage;
    155 		}
    156 
    157 		// Scale image.
    158 		if (scale != 1) {
    159 			int originalWidth = width, originalHeight = height;
    160 			width = Math.round(width * scale);
    161 			height = Math.round(height * scale);
    162 			BufferedImage newImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    163 			if (scale < 1) {
    164 				newImage.getGraphics().drawImage(image.getScaledInstance(width, height, Image.SCALE_AREA_AVERAGING), 0, 0, null);
    165 			} else {
    166 				Graphics2D g = (Graphics2D)newImage.getGraphics();
    167 				g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    168 				g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    169 				g.drawImage(image, 0, 0, width, height, null);
    170 			}
    171 			image = newImage;
    172 		}
    173 
    174 		if (isPatch) {
    175 			// Ninepatches aren't rotated or whitespace stripped.
    176 			rect = new Rect(image, 0, 0, width, height, true);
    177 			rect.splits = splits;
    178 			rect.pads = pads;
    179 			rect.canRotate = false;
    180 		} else {
    181 			rect = stripWhitespace(image);
    182 			if (rect == null) return null;
    183 		}
    184 
    185 		// Strip digits off end of name and use as index.
    186 		int index = -1;
    187 		if (settings.useIndexes) {
    188 			Matcher matcher = indexPattern.matcher(name);
    189 			if (matcher.matches()) {
    190 				name = matcher.group(1);
    191 				index = Integer.parseInt(matcher.group(2));
    192 			}
    193 		}
    194 
    195 		rect.name = name;
    196 		rect.index = index;
    197 		return rect;
    198 	}
    199 
    200 	/** Strips whitespace and returns the rect, or null if the image should be ignored. */
    201 	private Rect stripWhitespace (BufferedImage source) {
    202 		WritableRaster alphaRaster = source.getAlphaRaster();
    203 		if (alphaRaster == null || (!settings.stripWhitespaceX && !settings.stripWhitespaceY))
    204 			return new Rect(source, 0, 0, source.getWidth(), source.getHeight(), false);
    205 		final byte[] a = new byte[1];
    206 		int top = 0;
    207 		int bottom = source.getHeight();
    208 		if (settings.stripWhitespaceX) {
    209 			outer:
    210 			for (int y = 0; y < source.getHeight(); y++) {
    211 				for (int x = 0; x < source.getWidth(); x++) {
    212 					alphaRaster.getDataElements(x, y, a);
    213 					int alpha = a[0];
    214 					if (alpha < 0) alpha += 256;
    215 					if (alpha > settings.alphaThreshold) break outer;
    216 				}
    217 				top++;
    218 			}
    219 			outer:
    220 			for (int y = source.getHeight(); --y >= top;) {
    221 				for (int x = 0; x < source.getWidth(); x++) {
    222 					alphaRaster.getDataElements(x, y, a);
    223 					int alpha = a[0];
    224 					if (alpha < 0) alpha += 256;
    225 					if (alpha > settings.alphaThreshold) break outer;
    226 				}
    227 				bottom--;
    228 			}
    229 		}
    230 		int left = 0;
    231 		int right = source.getWidth();
    232 		if (settings.stripWhitespaceY) {
    233 			outer:
    234 			for (int x = 0; x < source.getWidth(); x++) {
    235 				for (int y = top; y < bottom; y++) {
    236 					alphaRaster.getDataElements(x, y, a);
    237 					int alpha = a[0];
    238 					if (alpha < 0) alpha += 256;
    239 					if (alpha > settings.alphaThreshold) break outer;
    240 				}
    241 				left++;
    242 			}
    243 			outer:
    244 			for (int x = source.getWidth(); --x >= left;) {
    245 				for (int y = top; y < bottom; y++) {
    246 					alphaRaster.getDataElements(x, y, a);
    247 					int alpha = a[0];
    248 					if (alpha < 0) alpha += 256;
    249 					if (alpha > settings.alphaThreshold) break outer;
    250 				}
    251 				right--;
    252 			}
    253 		}
    254 		int newWidth = right - left;
    255 		int newHeight = bottom - top;
    256 		if (newWidth <= 0 || newHeight <= 0) {
    257 			if (settings.ignoreBlankImages)
    258 				return null;
    259 			else
    260 				return new Rect(emptyImage, 0, 0, 1, 1, false);
    261 		}
    262 		return new Rect(source, left, top, newWidth, newHeight, false);
    263 	}
    264 
    265 	static private String splitError (int x, int y, int[] rgba, String name) {
    266 		throw new RuntimeException("Invalid " + name + " ninepatch split pixel at " + x + ", " + y + ", rgba: " + rgba[0] + ", "
    267 			+ rgba[1] + ", " + rgba[2] + ", " + rgba[3]);
    268 	}
    269 
    270 	/** Returns the splits, or null if the image had no splits or the splits were only a single region. Splits are an int[4] that
    271 	 * has left, right, top, bottom. */
    272 	private int[] getSplits (BufferedImage image, String name) {
    273 		WritableRaster raster = image.getRaster();
    274 
    275 		int startX = getSplitPoint(raster, name, 1, 0, true, true);
    276 		int endX = getSplitPoint(raster, name, startX, 0, false, true);
    277 		int startY = getSplitPoint(raster, name, 0, 1, true, false);
    278 		int endY = getSplitPoint(raster, name, 0, startY, false, false);
    279 
    280 		// Ensure pixels after the end are not invalid.
    281 		getSplitPoint(raster, name, endX + 1, 0, true, true);
    282 		getSplitPoint(raster, name, 0, endY + 1, true, false);
    283 
    284 		// No splits, or all splits.
    285 		if (startX == 0 && endX == 0 && startY == 0 && endY == 0) return null;
    286 
    287 		// Subtraction here is because the coordinates were computed before the 1px border was stripped.
    288 		if (startX != 0) {
    289 			startX--;
    290 			endX = raster.getWidth() - 2 - (endX - 1);
    291 		} else {
    292 			// If no start point was ever found, we assume full stretch.
    293 			endX = raster.getWidth() - 2;
    294 		}
    295 		if (startY != 0) {
    296 			startY--;
    297 			endY = raster.getHeight() - 2 - (endY - 1);
    298 		} else {
    299 			// If no start point was ever found, we assume full stretch.
    300 			endY = raster.getHeight() - 2;
    301 		}
    302 
    303 		if (scale != 1) {
    304 			startX = (int)Math.round(startX * scale);
    305 			endX = (int)Math.round(endX * scale);
    306 			startY = (int)Math.round(startY * scale);
    307 			endY = (int)Math.round(endY * scale);
    308 		}
    309 
    310 		return new int[] {startX, endX, startY, endY};
    311 	}
    312 
    313 	/** Returns the pads, or null if the image had no pads or the pads match the splits. Pads are an int[4] that has left, right,
    314 	 * top, bottom. */
    315 	private int[] getPads (BufferedImage image, String name, int[] splits) {
    316 		WritableRaster raster = image.getRaster();
    317 
    318 		int bottom = raster.getHeight() - 1;
    319 		int right = raster.getWidth() - 1;
    320 
    321 		int startX = getSplitPoint(raster, name, 1, bottom, true, true);
    322 		int startY = getSplitPoint(raster, name, right, 1, true, false);
    323 
    324 		// No need to hunt for the end if a start was never found.
    325 		int endX = 0;
    326 		int endY = 0;
    327 		if (startX != 0) endX = getSplitPoint(raster, name, startX + 1, bottom, false, true);
    328 		if (startY != 0) endY = getSplitPoint(raster, name, right, startY + 1, false, false);
    329 
    330 		// Ensure pixels after the end are not invalid.
    331 		getSplitPoint(raster, name, endX + 1, bottom, true, true);
    332 		getSplitPoint(raster, name, right, endY + 1, true, false);
    333 
    334 		// No pads.
    335 		if (startX == 0 && endX == 0 && startY == 0 && endY == 0) {
    336 			return null;
    337 		}
    338 
    339 		// -2 here is because the coordinates were computed before the 1px border was stripped.
    340 		if (startX == 0 && endX == 0) {
    341 			startX = -1;
    342 			endX = -1;
    343 		} else {
    344 			if (startX > 0) {
    345 				startX--;
    346 				endX = raster.getWidth() - 2 - (endX - 1);
    347 			} else {
    348 				// If no start point was ever found, we assume full stretch.
    349 				endX = raster.getWidth() - 2;
    350 			}
    351 		}
    352 		if (startY == 0 && endY == 0) {
    353 			startY = -1;
    354 			endY = -1;
    355 		} else {
    356 			if (startY > 0) {
    357 				startY--;
    358 				endY = raster.getHeight() - 2 - (endY - 1);
    359 			} else {
    360 				// If no start point was ever found, we assume full stretch.
    361 				endY = raster.getHeight() - 2;
    362 			}
    363 		}
    364 
    365 		if (scale != 1) {
    366 			startX = (int)Math.round(startX * scale);
    367 			endX = (int)Math.round(endX * scale);
    368 			startY = (int)Math.round(startY * scale);
    369 			endY = (int)Math.round(endY * scale);
    370 		}
    371 
    372 		int[] pads = new int[] {startX, endX, startY, endY};
    373 
    374 		if (splits != null && Arrays.equals(pads, splits)) {
    375 			return null;
    376 		}
    377 
    378 		return pads;
    379 	}
    380 
    381 	/** Hunts for the start or end of a sequence of split pixels. Begins searching at (startX, startY) then follows along the x or y
    382 	 * axis (depending on value of xAxis) for the first non-transparent pixel if startPoint is true, or the first transparent pixel
    383 	 * if startPoint is false. Returns 0 if none found, as 0 is considered an invalid split point being in the outer border which
    384 	 * will be stripped. */
    385 	static private int getSplitPoint (WritableRaster raster, String name, int startX, int startY, boolean startPoint, boolean xAxis) {
    386 		int[] rgba = new int[4];
    387 
    388 		int next = xAxis ? startX : startY;
    389 		int end = xAxis ? raster.getWidth() : raster.getHeight();
    390 		int breakA = startPoint ? 255 : 0;
    391 
    392 		int x = startX;
    393 		int y = startY;
    394 		while (next != end) {
    395 			if (xAxis)
    396 				x = next;
    397 			else
    398 				y = next;
    399 
    400 			raster.getPixel(x, y, rgba);
    401 			if (rgba[3] == breakA) return next;
    402 
    403 			if (!startPoint && (rgba[0] != 0 || rgba[1] != 0 || rgba[2] != 0 || rgba[3] != 255)) splitError(x, y, rgba, name);
    404 
    405 			next++;
    406 		}
    407 
    408 		return 0;
    409 	}
    410 
    411 	static private String hash (BufferedImage image) {
    412 		try {
    413 			MessageDigest digest = MessageDigest.getInstance("SHA1");
    414 
    415 			// Ensure image is the correct format.
    416 			int width = image.getWidth();
    417 			int height = image.getHeight();
    418 			if (image.getType() != BufferedImage.TYPE_INT_ARGB) {
    419 				BufferedImage newImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    420 				newImage.getGraphics().drawImage(image, 0, 0, null);
    421 				image = newImage;
    422 			}
    423 
    424 			WritableRaster raster = image.getRaster();
    425 			int[] pixels = new int[width];
    426 			for (int y = 0; y < height; y++) {
    427 				raster.getDataElements(0, y, width, 1, pixels);
    428 				for (int x = 0; x < width; x++)
    429 					hash(digest, pixels[x]);
    430 			}
    431 
    432 			hash(digest, width);
    433 			hash(digest, height);
    434 
    435 			return new BigInteger(1, digest.digest()).toString(16);
    436 		} catch (NoSuchAlgorithmException ex) {
    437 			throw new RuntimeException(ex);
    438 		}
    439 	}
    440 
    441 	static private void hash (MessageDigest digest, int value) {
    442 		digest.update((byte)(value >> 24));
    443 		digest.update((byte)(value >> 16));
    444 		digest.update((byte)(value >> 8));
    445 		digest.update((byte)value);
    446 	}
    447 }
    448