| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * 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.android.builder.png; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.annotations.VisibleForTesting; |
| import com.google.common.collect.Lists; |
| import com.google.common.io.Files; |
| |
| import java.awt.image.BufferedImage; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.zip.Deflater; |
| |
| import javax.imageio.ImageIO; |
| |
| /** |
| * a PngProcessor. |
| * |
| * It reads a png file and write another png file that's been optimized |
| * and processed in case of a 9-patch. |
| */ |
| public class PngProcessor { |
| |
| private static final int COLOR_WHITE = 0xFFFFFFFF; |
| private static final int COLOR_TICK = 0xFF000000; |
| private static final int COLOR_LAYOUT_BOUNDS_TICK = 0xFFFF0000; |
| |
| private static final int PNG_9PATCH_NO_COLOR = 0x00000001; |
| private static final int PNG_9PATCH_TRANSPARENT_COLOR = 0x00000000; |
| |
| |
| @NonNull |
| private final File mFile; |
| |
| private Chunk mIhdr; |
| private Chunk mIdat; |
| private List<Chunk> mOtherChunks = Lists.newArrayList(); |
| |
| /** |
| * Processes a given png and writes the resulting png file. |
| * @param from the input file |
| * @param to the destination file |
| * @throws IOException |
| * @throws NinePatchException |
| */ |
| public static void process(@NonNull File from, @NonNull File to) |
| throws IOException, NinePatchException { |
| PngProcessor processor = new PngProcessor(from); |
| processor.read(); |
| |
| if (!processor.is9Patch() && processor.size() >= from.length()) { |
| Files.copy(from, to); |
| return; |
| } |
| |
| PngWriter writer = new PngWriter(to); |
| writer.setIhdr(processor.getIhdr()) |
| .setChunks(processor.getOtherChunks()) |
| .setChunk(processor.getIdat()); |
| |
| writer.write(); |
| } |
| |
| public static void clearCache() { |
| ByteUtils.Cache.getCache().clear(); |
| } |
| |
| @VisibleForTesting |
| PngProcessor(@NonNull File file) { |
| checkNotNull(file); |
| |
| mFile = file; |
| } |
| |
| @VisibleForTesting |
| @NonNull |
| Chunk getIhdr() { |
| return mIhdr; |
| } |
| |
| @VisibleForTesting |
| @NonNull |
| List<Chunk> getOtherChunks() { |
| return mOtherChunks; |
| } |
| |
| @VisibleForTesting |
| @NonNull |
| Chunk getIdat() { |
| return mIdat; |
| } |
| |
| @VisibleForTesting |
| void read() throws IOException, NinePatchException { |
| BufferedImage image = ImageIO.read(mFile); |
| |
| processImageContent(image); |
| } |
| |
| private void addChunk(@NonNull Chunk chunk) { |
| mOtherChunks.add(chunk); |
| } |
| |
| /** |
| * Returns the size of the generated png. |
| */ |
| long size() { |
| long size = PngWriter.SIGNATURE.length; |
| |
| size += mIhdr.size(); |
| size += mIdat.size(); |
| for (Chunk chunk : mOtherChunks) { |
| size += chunk.size(); |
| } |
| |
| return size; |
| } |
| |
| private void processImageContent(@NonNull BufferedImage image) throws NinePatchException, |
| IOException { |
| int width = image.getWidth(); |
| int height = image.getHeight(); |
| |
| int[] content = new int[width * height]; |
| |
| image.getRGB(0, 0, width, height, content, 0, width); |
| |
| int startX = 0; |
| int startY = 0; |
| int endX = width; |
| int endY = height; |
| |
| if (is9Patch()) { |
| startX = 1; |
| startY = 1; |
| endX--; |
| endY--; |
| |
| processBorder(content, width, height); |
| } |
| |
| ColorType colorType = createImage(content, width, startX, endX, startY, endY, is9Patch()); |
| |
| mIhdr = computeIhdr(endX - startX, endY - startY, (byte) 8, colorType); |
| } |
| |
| private void writeIDat(byte[] data) throws IOException { |
| // create a growing buffer for the result. |
| ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length); |
| |
| Deflater deflater = new Deflater(Deflater.DEFLATED); |
| deflater.setInput(data); |
| deflater.finish(); |
| |
| // temp buffer for compressed data. |
| byte[] tmpBuffer = new byte[1024]; |
| while (!deflater.finished()) { |
| int compressedLen = deflater.deflate(tmpBuffer); |
| bos.write(tmpBuffer, 0, compressedLen); |
| } |
| |
| bos.close(); |
| |
| byte[] compressedData = bos.toByteArray(); |
| |
| mIdat = new Chunk(PngWriter.IDAT, compressedData); |
| } |
| |
| /** |
| * Creates the PNG image buffer to be encoded in IDAT. |
| * |
| * This figures out the smallest way to encode it, and creates the IDAT chunk (and other needed |
| * chunks like PLTE). |
| * |
| * @param content the image array in ARGB. |
| * @param scanline the scanline value for the image buffer |
| * @param startX the startX of the image we want to create from the buffer |
| * @param endX the endX of the image we want to create from the buffer |
| * @param startY the startY of the image we want to create from the buffer |
| * @param endY the endY of the image we want to create from the buffer |
| * @param is9Patch whether the image is a nine-patch |
| */ |
| private ColorType createImage(@NonNull int[] content, int scanline, |
| int startX, int endX, int startY, int endY, boolean is9Patch) throws IOException { |
| int[] paletteColors = new int[256]; |
| int paletteColorCount = 0; |
| |
| int grayscaleTolerance = -1; |
| int maxGrayDeviation = 0; |
| |
| boolean isOpaque = true; |
| boolean isPalette = true; |
| boolean isGrayscale = true; |
| |
| int width = endX - startX; |
| int height = endY - startY; |
| |
| // RGBA buffer, in case it's the best one. |
| int rgbaLen = (1 + width * 4) * height; |
| ByteBuffer rgbaBufer = ByteBuffer.allocate(rgbaLen); |
| |
| // Store palette data optimistically in case we use palette mode. |
| // Better than regoing through content after. |
| int indexedLen = (1 + width) * height; |
| byte[] indexedContent = new byte[indexedLen]; |
| int indexedContentIndex = 0; |
| |
| // Scan the entire image and determine if: |
| // 1. Every pixel has R == G == B (grayscale) |
| // 2. Every pixel has A == 255 (opaque) |
| // 3. There are no more than 256 distinct RGBA colors |
| for (int y = startY ; y < endY ; y++) { |
| rgbaBufer.put((byte) 0); |
| indexedContent[indexedContentIndex++] = 0; |
| |
| for (int x = startX ; x < endX ; x++) { |
| int argb = content[(scanline * y) + x]; |
| |
| int aa = argb >>> 24; |
| int rr = (argb >> 16) & 0x000000FF; |
| int gg = (argb >> 8) & 0x000000FF; |
| int bb = argb & 0x000000FF; |
| |
| int odev = maxGrayDeviation; |
| maxGrayDeviation = Math.max(Math.abs(rr - gg), maxGrayDeviation); |
| maxGrayDeviation = Math.max(Math.abs(gg - bb), maxGrayDeviation); |
| maxGrayDeviation = Math.max(Math.abs(bb - rr), maxGrayDeviation); |
| |
| // Check if image is really grayscale |
| if (isGrayscale && (rr != gg || rr != bb)) { |
| isGrayscale = false; |
| } |
| |
| // Check if image is really opaque |
| if (isOpaque && aa != 0xff) { |
| isOpaque = false; |
| } |
| |
| // Check if image is really <= 256 colors |
| if (isPalette) { |
| int rgba = (argb << 8) | aa; |
| |
| boolean match = false; |
| int idx; |
| for (idx = 0; idx < paletteColorCount; idx++) { |
| if (paletteColors[idx] == rgba) { |
| match = true; |
| break; |
| } |
| } |
| |
| // Write the palette index for the pixel to outRows optimistically |
| // We might overwrite it later if we decide to encode as gray or |
| // gray + alpha |
| indexedContent[indexedContentIndex++] = (byte)idx; |
| if (!match) { |
| if (paletteColorCount == 256) { |
| isPalette = false; |
| } else { |
| paletteColors[paletteColorCount++] = rgba; |
| } |
| } |
| } |
| |
| // write rgba optimistically |
| rgbaBufer.putInt((argb << 8) | aa); |
| } |
| } |
| |
| boolean hasTransparency = !isOpaque; |
| |
| int bpp = isOpaque ? 3 : 4; |
| int paletteSize = width * height + (isOpaque ? 3 : 4) * paletteColorCount; |
| |
| ColorType colorType; |
| |
| // Choose the best color type for the image. |
| // 1. Opaque gray - use COLOR_TYPE_GRAY at 1 byte/pixel |
| // 2. Gray + alpha - use COLOR_TYPE_PALETTE if the number of distinct combinations |
| // is sufficiently small, otherwise use COLOR_TYPE_GRAY_ALPHA |
| // 3. RGB(A) - use COLOR_TYPE_PALETTE if the number of distinct colors is sufficiently |
| // small, otherwise use COLOR_TYPE_RGB{_ALPHA} |
| if (isGrayscale) { |
| if (isOpaque) { |
| colorType = ColorType.GRAY_SCALE; // 1 byte/pixel |
| } else { |
| // Use a simple heuristic to determine whether using a palette will |
| // save space versus using gray + alpha for each pixel. |
| // This doesn't take into account chunk overhead, filtering, LZ |
| // compression, etc. |
| if (isPalette && paletteSize < 2 * width * height) { |
| colorType = ColorType.PLTE; // 1 byte/pixel + 4 bytes/color |
| } else { |
| colorType = ColorType.GRAY_SCALE_ALPHA; // 2 bytes per pixel |
| } |
| } |
| } else if (isPalette && paletteSize < bpp * width * height) { |
| colorType = ColorType.PLTE; // 1 byte/pixel + 4 bytes/color |
| } else { |
| if (maxGrayDeviation <= grayscaleTolerance) { |
| colorType = isOpaque ? ColorType.GRAY_SCALE : ColorType.GRAY_SCALE_ALPHA; |
| } else { |
| colorType = isOpaque ? ColorType.RGB : ColorType.RGBA; |
| } |
| } |
| |
| // If the image is a 9-patch, we need to preserve it as a ARGB file to make |
| // sure the pixels will not be pre-dithered/clamped until we decide they are |
| if (is9Patch && (colorType == ColorType.RGB || |
| colorType == ColorType.GRAY_SCALE || colorType == ColorType.PLTE)) { |
| colorType = ColorType.RGBA; |
| } |
| |
| // Perform postprocessing of the image or palette data based on the final |
| // color type chosen |
| if (colorType == ColorType.PLTE) { |
| byte[] rgbPalette = new byte[paletteColorCount * 3]; |
| byte[] alphaPalette = null; |
| if (hasTransparency) { |
| alphaPalette = new byte[paletteColorCount]; |
| } |
| |
| // Create the RGB and alpha palettes |
| for (int idx = 0; idx < paletteColorCount; idx++) { |
| int color = paletteColors[idx]; |
| rgbPalette[idx * 3] = (byte) ( color >>> 24); |
| rgbPalette[idx * 3 + 1] = (byte) ((color >> 16) & 0xFF); |
| rgbPalette[idx * 3 + 2] = (byte) ((color >> 8) & 0xFF); |
| if (hasTransparency) { |
| alphaPalette[idx] = (byte) (color & 0xFF); |
| } |
| } |
| |
| // create chunks. |
| addChunk(new Chunk(PngWriter.PLTE, rgbPalette)); |
| |
| if (hasTransparency) { |
| addChunk(new Chunk(PngWriter.TRNS, alphaPalette)); |
| } |
| |
| // create image data chunk |
| writeIDat(indexedContent); |
| |
| } else if (colorType == ColorType.GRAY_SCALE || colorType == ColorType.GRAY_SCALE_ALPHA) { |
| int grayLen = (1 + width * (1 + (hasTransparency ? 1 : 0))) * height; |
| byte[] grayContent = new byte[grayLen]; |
| int grayContentIndex = 0; |
| |
| for (int y = startY ; y < endY ; y++) { |
| grayContent[grayContentIndex++] = 0; |
| |
| for (int x = startX ; x < endX ; x++) { |
| int argb = content[(scanline * y) + x]; |
| |
| int rr = (argb >> 16) & 0x000000FF; |
| |
| if (isGrayscale) { |
| grayContent[grayContentIndex++] = (byte) rr; |
| } else { |
| int gg = (argb >> 8) & 0x000000FF; |
| int bb = argb & 0x000000FF; |
| |
| // convert RGB to Grayscale. |
| // Ref: http://en.wikipedia.org/wiki/Luma_(video) |
| grayContent[grayContentIndex++] = (byte) (rr * 0.2126f + gg * 0.7152f + bb * 0.0722f); |
| } |
| |
| if (hasTransparency) { |
| int aa = argb >>> 24; |
| grayContent[grayContentIndex++] = (byte) aa; |
| } |
| } |
| } |
| |
| // create image data chunk |
| writeIDat(grayContent); |
| |
| } else if (colorType == ColorType.RGBA) { |
| writeIDat(rgbaBufer.array()); |
| } else { |
| //RGB mode |
| int rgbLen = (1 + width * 3) * height; |
| byte[] rgbContent = new byte[rgbLen]; |
| int rgbContentIndex = 0; |
| |
| for (int y = startY ; y < endY ; y++) { |
| rgbContent[rgbContentIndex++] = 0; |
| |
| for (int x = startX ; x < endX ; x++) { |
| int argb = content[(scanline * y) + x]; |
| |
| rgbContent[rgbContentIndex++] = (byte) ((argb >> 16) & 0x000000FF); |
| rgbContent[rgbContentIndex++] = (byte) ((argb >> 8) & 0x000000FF); |
| rgbContent[rgbContentIndex++] = (byte) ( argb & 0x000000FF); |
| } |
| } |
| |
| // create image data chunk |
| writeIDat(rgbContent); |
| } |
| |
| return colorType; |
| } |
| |
| /** |
| * process the border of the image to find 9-patch info |
| * @param content the content of ARGB |
| * @param width the width |
| * @param height the height |
| * @throws NinePatchException |
| */ |
| private void processBorder(int[] content, int width, int height) |
| throws NinePatchException { |
| // Validate size... |
| if (width < 3 || height < 3) { |
| throw new NinePatchException(mFile, "Image must be at least 3x3 (1x1 without frame) pixels"); |
| } |
| |
| int i, j; |
| |
| int[] xDivs = new int[width]; |
| int[] yDivs = new int[height]; |
| int[] colors; |
| Arrays.fill(xDivs, -1); |
| Arrays.fill(yDivs, -1); |
| |
| int numXDivs; |
| int numYDivs; |
| byte numColors; |
| int numRows, numCols; |
| int top, left, right, bottom; |
| |
| int paddingLeft, paddingTop, paddingRight, paddingBottom; |
| |
| boolean transparent = (content[0] & 0xFF000000) == 0; |
| |
| int colorIndex = 0; |
| |
| // Validate frame... |
| if (!transparent && content[0] != 0xFFFFFFFF) { |
| throw new NinePatchException(mFile, |
| "Must have one-pixel frame that is either transparent or white"); |
| } |
| |
| // Find left and right of sizing areas... |
| AtomicInteger outInt = new AtomicInteger(0); |
| try { |
| getHorizontalTicks( |
| content, 0, width, |
| transparent, true /*required*/, |
| xDivs, 0, 1, outInt, |
| true /*multipleAllowed*/); |
| numXDivs = outInt.get(); |
| } catch (TickException e) { |
| throw new NinePatchException(mFile, e, "top"); |
| } |
| |
| // Find top and bottom of sizing areas... |
| outInt.set(0); |
| try { |
| getVerticalTicks(content, 0, width, height, |
| transparent, true /*required*/, |
| yDivs, 0, 1, outInt, |
| true /*multipleAllowed*/); |
| numYDivs = outInt.get(); |
| } catch (TickException e) { |
| throw new NinePatchException(mFile, e, "left"); |
| } |
| |
| // Find left and right of padding area... |
| int[] values = new int[2]; |
| try { |
| getHorizontalTicks( |
| content, width * (height - 1), width, |
| transparent, false /*required*/, |
| values, 0, 1, null, |
| false /*multipleAllowed*/); |
| paddingLeft = values[0]; |
| paddingRight = values[1]; |
| values[0] = values[1] = 0; |
| } catch (TickException e) { |
| throw new NinePatchException(mFile, e, "bottom"); |
| } |
| |
| // Find top and bottom of padding area... |
| try { |
| getVerticalTicks( |
| content, width - 1, width, height, |
| transparent, false /*required*/, |
| values, 0, 1, null, |
| false /*multipleAllowed*/); |
| paddingTop = values[0]; |
| paddingBottom = values[1]; |
| } catch (TickException e) { |
| throw new NinePatchException(mFile, e, "right"); |
| } |
| |
| try { |
| // Find left and right of layout padding... |
| getHorizontalLayoutBoundsTicks(content, width * (height - 1), width, |
| transparent, false, values); |
| } catch (TickException e) { |
| throw new NinePatchException(mFile, e, "bottom"); |
| } |
| |
| int[] values2 = new int[2]; |
| try { |
| getVerticalLayoutBoundsTicks(content, width - 1, width, height, |
| transparent, false, values2); |
| } catch (TickException e) { |
| throw new NinePatchException(mFile, e, "right"); |
| } |
| |
| LayoutBoundChunkBuilder layoutBoundChunkBuilder = null; |
| if (values[0] != 0 || values[1] != 0 || values2[0] != 0 || values2[1] != 0) { |
| layoutBoundChunkBuilder = new LayoutBoundChunkBuilder( |
| values[0], values2[0], values[1], values2[1]); |
| } |
| |
| // If padding is not yet specified, take values from size. |
| if (paddingLeft < 0) { |
| paddingLeft = xDivs[0]; |
| paddingRight = width - 2 - xDivs[1]; |
| } else { |
| // Adjust value to be correct! |
| paddingRight = width - 2 - paddingRight; |
| } |
| if (paddingTop < 0) { |
| paddingTop = yDivs[0]; |
| paddingBottom = height - 2 - yDivs[1]; |
| } else { |
| // Adjust value to be correct! |
| paddingBottom = height - 2 - paddingBottom; |
| } |
| |
| // Remove frame from image. |
| width -= 2; |
| height -= 2; |
| |
| // Figure out the number of rows and columns in the N-patch |
| numCols = numXDivs + 1; |
| if (xDivs[0] == 0) { // Column 1 is strechable |
| numCols--; |
| } |
| if (xDivs[numXDivs - 1] == width) { |
| numCols--; |
| } |
| numRows = numYDivs + 1; |
| if (yDivs[0] == 0) { // Row 1 is strechable |
| numRows--; |
| } |
| if (yDivs[numYDivs - 1] == height) { |
| numRows--; |
| } |
| |
| // Make sure the amount of rows and columns will fit in the number of |
| // colors we can use in the 9-patch format. |
| if (numRows * numCols > 0x7F) { |
| throw new NinePatchException(mFile, "Too many rows and columns in 9-patch perimeter"); |
| } |
| |
| numColors = (byte) (numRows * numCols); |
| colors = new int[numColors]; |
| |
| // Fill in color information for each patch. |
| |
| int c; |
| top = 0; |
| |
| // The first row always starts with the top being at y=0 and the bottom |
| // being either yDivs[1] (if yDivs[0]=0) of yDivs[0]. In the former case |
| // the first row is stretchable along the Y axis, otherwise it is fixed. |
| // The last row always ends with the bottom being bitmap.height and the top |
| // being either yDivs[numYDivs-2] (if yDivs[numYDivs-1]=bitmap.height) or |
| // yDivs[numYDivs-1]. In the former case the last row is stretchable along |
| // the Y axis, otherwise it is fixed. |
| // |
| // The first and last columns are similarly treated with respect to the X |
| // axis. |
| // |
| // The above is to help explain some of the special casing that goes on the |
| // code below. |
| |
| // The initial yDiv and whether the first row is considered stretchable or |
| // not depends on whether yDiv[0] was zero or not. |
| for (j = (yDivs[0] == 0 ? 1 : 0); |
| j <= numYDivs && top < height; |
| j++) { |
| if (j == numYDivs) { |
| bottom = height; |
| } else { |
| bottom = yDivs[j]; |
| } |
| left = 0; |
| // The initial xDiv and whether the first column is considered |
| // stretchable or not depends on whether xDiv[0] was zero or not. |
| for (i = xDivs[0] == 0 ? 1 : 0; |
| i <= numXDivs && left < width; |
| i++) { |
| if (i == numXDivs) { |
| right = width; |
| } else { |
| right = xDivs[i]; |
| } |
| c = getColor(content, width + 2, left, top, right - 1, bottom - 1); |
| colors[colorIndex++] = c; |
| left = right; |
| } |
| top = bottom; |
| } |
| |
| // Create the chunks. |
| NinePatchChunkBuilder ninePatchChunkBuilder = new NinePatchChunkBuilder( |
| xDivs, numXDivs, yDivs, numYDivs, colors, |
| paddingLeft, paddingRight, paddingTop, paddingBottom); |
| |
| addChunk(ninePatchChunkBuilder.getChunk()); |
| if (layoutBoundChunkBuilder != null) { |
| addChunk(layoutBoundChunkBuilder.getChunk()); |
| } |
| } |
| |
| /** |
| * returns a color. the top/left/right/bottom coordinate are in a subframe of content, starting |
| * in (1,1). |
| * @param content the image buffer |
| * @param width the width of the image buffer |
| * @param left left coordinate. |
| * @param top top coordinate. |
| * @param right right coordinate. |
| * @param bottom bottom coordinate. |
| * @return a color. |
| */ |
| static int getColor(@NonNull int[] content, int width, |
| int left, int top, int right, int bottom) { |
| int color = content[(top + 1) * width + left + 1]; |
| int alpha = color & 0xFF000000; |
| |
| if (left > right || top > bottom) { |
| return PNG_9PATCH_TRANSPARENT_COLOR; |
| } |
| |
| while (top <= bottom) { |
| for (int i = left; i <= right; i++) { |
| int c = content[(top + 1) * width + i + 1]; |
| if (alpha == 0) { |
| if ((c & 0xFF000000) != 0) { |
| return PNG_9PATCH_NO_COLOR; |
| } |
| } else if (c != color) { |
| return PNG_9PATCH_NO_COLOR; |
| } |
| } |
| top++; |
| } |
| |
| if (alpha == 0) { |
| return PNG_9PATCH_TRANSPARENT_COLOR; |
| } |
| |
| return color; |
| } |
| |
| |
| private static enum TickType { |
| NONE, TICK, LAYOUT_BOUNDS, BOTH |
| } |
| |
| @NonNull |
| private static TickType getTickType(int color, boolean transparent) throws TickException { |
| |
| int alpha = color >>> 24; |
| |
| if (transparent) { |
| if (alpha == 0) { |
| return TickType.NONE; |
| } |
| if (color == COLOR_LAYOUT_BOUNDS_TICK) { |
| return TickType.LAYOUT_BOUNDS; |
| } |
| if (color == COLOR_TICK) { |
| return TickType.TICK; |
| } |
| |
| // Error cases |
| if (alpha != 0xFF) { |
| throw TickException.createWithColor( |
| "Frame pixels must be either solid or transparent (not intermediate alphas)", |
| color); |
| } |
| if ((color & 0x00FFFFFF) != 0) { |
| throw TickException.createWithColor("Ticks in transparent frame must be black or red", |
| color); |
| } |
| return TickType.TICK; |
| } |
| |
| if (alpha != 0xFF) { |
| throw TickException.createWithColor("White frame must be a solid color (no alpha)", |
| color); |
| } |
| if (color == COLOR_WHITE) { |
| return TickType.NONE; |
| } |
| if (color == COLOR_TICK) { |
| return TickType.TICK; |
| } |
| if (color == COLOR_LAYOUT_BOUNDS_TICK) { |
| return TickType.LAYOUT_BOUNDS; |
| } |
| |
| if ((color & 0x00FFFFFF) != 0) { |
| throw TickException.createWithColor("Ticks in transparent frame must be black or red", |
| color); |
| } |
| |
| return TickType.TICK; |
| } |
| |
| |
| private static enum Tick { |
| START, INSIDE_1, OUTSIDE_1 |
| } |
| |
| private static void getHorizontalTicks( |
| @NonNull int[] content, int offset, int width, |
| boolean transparent, boolean required, |
| @NonNull int[] divs, int left, int right, |
| @Nullable AtomicInteger outDivs, boolean multipleAllowed) throws TickException { |
| int i; |
| divs[left] = divs[right] = -1; |
| Tick state = Tick.START; |
| boolean found = false; |
| |
| for (i = 1; i < width - 1; i++) { |
| TickType tickType; |
| try { |
| tickType = getTickType(content[offset + i], transparent); |
| } catch (TickException e) { |
| throw new TickException(e, i); |
| } |
| |
| if (TickType.TICK == tickType) { |
| if (state == Tick.START || |
| (state == Tick.OUTSIDE_1 && multipleAllowed)) { |
| divs[left] = i - 1; |
| divs[right] = width - 2; |
| found = true; |
| if (outDivs != null) { |
| outDivs.addAndGet(2); |
| } |
| state = Tick.INSIDE_1; |
| } else if (state == Tick.OUTSIDE_1) { |
| throw new TickException("Can't have more than one marked region along edge"); |
| } |
| } else { |
| if (state == Tick.INSIDE_1) { |
| // We're done with this div. Move on to the next. |
| divs[right] = i - 1; |
| right += 2; |
| left += 2; |
| state = Tick.OUTSIDE_1; |
| } |
| } |
| |
| } |
| |
| if (required && !found) { |
| throw new TickException("No marked region found along edge"); |
| } |
| } |
| |
| private static void getVerticalTicks( |
| @NonNull int[] content, int offset, int width, int height, |
| boolean transparent, boolean required, |
| @NonNull int[] divs, int top, int bottom, |
| @Nullable AtomicInteger outDivs, boolean multipleAllowed) throws TickException { |
| |
| int i; |
| divs[top] = divs[bottom] = -1; |
| Tick state = Tick.START; |
| boolean found = false; |
| |
| for (i = 1; i < height - 1; i++) { |
| TickType tickType; |
| try { |
| tickType = getTickType(content[offset + width * i], transparent); |
| } catch (TickException e) { |
| throw new TickException(e, i); |
| } |
| |
| if (TickType.TICK == tickType) { |
| if (state == Tick.START || |
| (state == Tick.OUTSIDE_1 && multipleAllowed)) { |
| divs[top] = i - 1; |
| divs[bottom] = height - 2; |
| found = true; |
| if (outDivs != null) { |
| outDivs.addAndGet(2); |
| } |
| state = Tick.INSIDE_1; |
| } else if (state == Tick.OUTSIDE_1) { |
| throw new TickException("Can't have more than one marked region along edge"); |
| } |
| } else { |
| if (state == Tick.INSIDE_1) { |
| // We're done with this div. Move on to the next. |
| divs[bottom] = i - 1; |
| top += 2; |
| bottom += 2; |
| state = Tick.OUTSIDE_1; |
| } |
| } |
| } |
| |
| if (required && !found) { |
| throw new TickException("No marked region found along edge"); |
| } |
| } |
| |
| private static void getHorizontalLayoutBoundsTicks( |
| @NonNull int[] content, int offset, int width, boolean transparent, boolean required, |
| @NonNull int[] outValues) throws TickException { |
| |
| int i; |
| outValues[0] = outValues[1] = 0; |
| |
| // Look for left tick |
| if (TickType.LAYOUT_BOUNDS == getTickType(content[offset + 1], transparent)) { |
| // Starting with a layout padding tick |
| i = 1; |
| while (i < width - 1) { |
| outValues[0]++; |
| i++; |
| TickType tick = getTickType(content[offset + i], transparent); |
| if (tick != TickType.LAYOUT_BOUNDS) { |
| break; |
| } |
| } |
| } |
| |
| // Look for right tick |
| if (TickType.LAYOUT_BOUNDS == getTickType(content[offset + (width - 2)], transparent)) { |
| // Ending with a layout padding tick |
| i = width - 2; |
| while (i > 1) { |
| outValues[1]++; |
| i--; |
| TickType tick = getTickType(content[offset + i], transparent); |
| if (tick != TickType.LAYOUT_BOUNDS) { |
| break; |
| } |
| } |
| } |
| } |
| |
| private static void getVerticalLayoutBoundsTicks( |
| @NonNull int[] content, int offset, int width, int height, |
| boolean transparent, boolean required, |
| @NonNull int[] outValues) throws TickException { |
| int i; |
| outValues[0] = outValues[1] = 0; |
| |
| // Look for top tick |
| if (TickType.LAYOUT_BOUNDS == getTickType(content[offset + width], transparent)) { |
| // Starting with a layout padding tick |
| i = 1; |
| while (i < height - 1) { |
| outValues[0]++; |
| i++; |
| TickType tick = getTickType(content[offset + width * i], transparent); |
| if (tick != TickType.LAYOUT_BOUNDS) { |
| break; |
| } |
| } |
| } |
| |
| // Look for bottom tick |
| if (TickType.LAYOUT_BOUNDS == getTickType(content[offset + width * (height - 2)], |
| transparent)) { |
| // Ending with a layout padding tick |
| i = height - 2; |
| while (i > 1) { |
| outValues[1]++; |
| i--; |
| TickType tick = getTickType(content[offset + width * i], transparent); |
| if (tick != TickType.LAYOUT_BOUNDS) { |
| break; |
| } |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| Chunk computeIhdr(int width, int height, byte bitDepth, @NonNull ColorType colorType) { |
| byte[] buffer = new byte[13]; |
| |
| ByteUtils utils = ByteUtils.Cache.get(); |
| |
| System.arraycopy(utils.getIntAsArray(width), 0, buffer, 0, 4); |
| System.arraycopy(utils.getIntAsArray(height), 0, buffer, 4, 4); |
| buffer[8] = bitDepth; |
| buffer[9] = colorType.getFlag(); |
| buffer[10] = 0; // compressionMethod |
| buffer[11] = 0; // filterMethod; |
| buffer[12] = 0; // interlaceMethod |
| |
| return new Chunk(PngWriter.IHDR, buffer); |
| } |
| |
| boolean is9Patch() { |
| return mFile.getPath().endsWith(SdkConstants.DOT_9PNG); |
| } |
| } |