All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.builder.png.PngProcessor Maven / Gradle / Ivy

There is a newer version: 1.3.1
Show newest version
/*
 * 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 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 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);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy