blob: f1b9871498dbb97b9b406a0fdb9ecbc69f0921a7 [file] [log] [blame]
/*
* 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);
}
}