summaryrefslogtreecommitdiffstats
path: root/tools/aapt2/Png.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tools/aapt2/Png.cpp')
-rw-r--r--tools/aapt2/Png.cpp1280
1 files changed, 1280 insertions, 0 deletions
diff --git a/tools/aapt2/Png.cpp b/tools/aapt2/Png.cpp
new file mode 100644
index 0000000..4e9b68e
--- /dev/null
+++ b/tools/aapt2/Png.cpp
@@ -0,0 +1,1280 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include "BigBuffer.h"
+#include "Logger.h"
+#include "Png.h"
+#include "Source.h"
+#include "Util.h"
+
+#include <androidfw/ResourceTypes.h>
+#include <iostream>
+#include <png.h>
+#include <sstream>
+#include <string>
+#include <vector>
+#include <zlib.h>
+
+namespace aapt {
+
+constexpr bool kDebug = false;
+constexpr size_t kPngSignatureSize = 8u;
+
+struct PngInfo {
+ ~PngInfo() {
+ for (png_bytep row : rows) {
+ if (row != nullptr) {
+ delete[] row;
+ }
+ }
+
+ delete[] xDivs;
+ delete[] yDivs;
+ }
+
+ void* serialize9Patch() {
+ void* serialized = android::Res_png_9patch::serialize(info9Patch, xDivs, yDivs,
+ colors.data());
+ reinterpret_cast<android::Res_png_9patch*>(serialized)->deviceToFile();
+ return serialized;
+ }
+
+ uint32_t width = 0;
+ uint32_t height = 0;
+ std::vector<png_bytep> rows;
+
+ bool is9Patch = false;
+ android::Res_png_9patch info9Patch;
+ int32_t* xDivs = nullptr;
+ int32_t* yDivs = nullptr;
+ std::vector<uint32_t> colors;
+
+ // Layout padding.
+ bool haveLayoutBounds = false;
+ int32_t layoutBoundsLeft;
+ int32_t layoutBoundsTop;
+ int32_t layoutBoundsRight;
+ int32_t layoutBoundsBottom;
+
+ // Round rect outline description.
+ int32_t outlineInsetsLeft;
+ int32_t outlineInsetsTop;
+ int32_t outlineInsetsRight;
+ int32_t outlineInsetsBottom;
+ float outlineRadius;
+ uint8_t outlineAlpha;
+};
+
+static void readDataFromStream(png_structp readPtr, png_bytep data, png_size_t length) {
+ std::istream* input = reinterpret_cast<std::istream*>(png_get_io_ptr(readPtr));
+ if (!input->read(reinterpret_cast<char*>(data), length)) {
+ png_error(readPtr, strerror(errno));
+ }
+}
+
+static void writeDataToStream(png_structp writePtr, png_bytep data, png_size_t length) {
+ BigBuffer* outBuffer = reinterpret_cast<BigBuffer*>(png_get_io_ptr(writePtr));
+ png_bytep buf = outBuffer->nextBlock<png_byte>(length);
+ memcpy(buf, data, length);
+}
+
+static void flushDataToStream(png_structp /*writePtr*/) {
+}
+
+static void logWarning(png_structp readPtr, png_const_charp warningMessage) {
+ SourceLogger* logger = reinterpret_cast<SourceLogger*>(png_get_error_ptr(readPtr));
+ logger->warn() << warningMessage << "." << std::endl;
+}
+
+
+static bool readPng(png_structp readPtr, png_infop infoPtr, PngInfo* outInfo,
+ std::string* outError) {
+ if (setjmp(png_jmpbuf(readPtr))) {
+ *outError = "failed reading png";
+ return false;
+ }
+
+ png_set_sig_bytes(readPtr, kPngSignatureSize);
+ png_read_info(readPtr, infoPtr);
+
+ int colorType, bitDepth, interlaceType, compressionType;
+ png_get_IHDR(readPtr, infoPtr, &outInfo->width, &outInfo->height, &bitDepth, &colorType,
+ &interlaceType, &compressionType, nullptr);
+
+ if (colorType == PNG_COLOR_TYPE_PALETTE) {
+ png_set_palette_to_rgb(readPtr);
+ }
+
+ if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8) {
+ png_set_expand_gray_1_2_4_to_8(readPtr);
+ }
+
+ if (png_get_valid(readPtr, infoPtr, PNG_INFO_tRNS)) {
+ png_set_tRNS_to_alpha(readPtr);
+ }
+
+ if (bitDepth == 16) {
+ png_set_strip_16(readPtr);
+ }
+
+ if (!(colorType & PNG_COLOR_MASK_ALPHA)) {
+ png_set_add_alpha(readPtr, 0xFF, PNG_FILLER_AFTER);
+ }
+
+ if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) {
+ png_set_gray_to_rgb(readPtr);
+ }
+
+ png_set_interlace_handling(readPtr);
+ png_read_update_info(readPtr, infoPtr);
+
+ const uint32_t rowBytes = png_get_rowbytes(readPtr, infoPtr);
+ outInfo->rows.resize(outInfo->height);
+ for (size_t i = 0; i < outInfo->height; i++) {
+ outInfo->rows[i] = new png_byte[rowBytes];
+ }
+
+ png_read_image(readPtr, outInfo->rows.data());
+ png_read_end(readPtr, infoPtr);
+ return true;
+}
+
+static void checkNinePatchSerialization(android::Res_png_9patch* inPatch, void* data) {
+ size_t patchSize = inPatch->serializedSize();
+ void* newData = malloc(patchSize);
+ memcpy(newData, data, patchSize);
+ android::Res_png_9patch* outPatch = inPatch->deserialize(newData);
+ outPatch->fileToDevice();
+ // deserialization is done in place, so outPatch == newData
+ assert(outPatch == newData);
+ assert(outPatch->numXDivs == inPatch->numXDivs);
+ assert(outPatch->numYDivs == inPatch->numYDivs);
+ assert(outPatch->paddingLeft == inPatch->paddingLeft);
+ assert(outPatch->paddingRight == inPatch->paddingRight);
+ assert(outPatch->paddingTop == inPatch->paddingTop);
+ assert(outPatch->paddingBottom == inPatch->paddingBottom);
+/* for (int i = 0; i < outPatch->numXDivs; i++) {
+ assert(outPatch->getXDivs()[i] == inPatch->getXDivs()[i]);
+ }
+ for (int i = 0; i < outPatch->numYDivs; i++) {
+ assert(outPatch->getYDivs()[i] == inPatch->getYDivs()[i]);
+ }
+ for (int i = 0; i < outPatch->numColors; i++) {
+ assert(outPatch->getColors()[i] == inPatch->getColors()[i]);
+ }*/
+ free(newData);
+}
+
+/*static void dump_image(int w, int h, const png_byte* const* rows, int color_type) {
+ int i, j, rr, gg, bb, aa;
+
+ int bpp;
+ if (color_type == PNG_COLOR_TYPE_PALETTE || color_type == PNG_COLOR_TYPE_GRAY) {
+ bpp = 1;
+ } else if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
+ bpp = 2;
+ } else if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA) {
+ // We use a padding byte even when there is no alpha
+ bpp = 4;
+ } else {
+ printf("Unknown color type %d.\n", color_type);
+ }
+
+ for (j = 0; j < h; j++) {
+ const png_byte* row = rows[j];
+ for (i = 0; i < w; i++) {
+ rr = row[0];
+ gg = row[1];
+ bb = row[2];
+ aa = row[3];
+ row += bpp;
+
+ if (i == 0) {
+ printf("Row %d:", j);
+ }
+ switch (bpp) {
+ case 1:
+ printf(" (%d)", rr);
+ break;
+ case 2:
+ printf(" (%d %d", rr, gg);
+ break;
+ case 3:
+ printf(" (%d %d %d)", rr, gg, bb);
+ break;
+ case 4:
+ printf(" (%d %d %d %d)", rr, gg, bb, aa);
+ break;
+ }
+ if (i == (w - 1)) {
+ printf("\n");
+ }
+ }
+ }
+}*/
+
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#define ABS(a) ((a)<0?-(a):(a))
+
+static void analyze_image(SourceLogger* logger, const PngInfo& imageInfo, int grayscaleTolerance,
+ png_colorp rgbPalette, png_bytep alphaPalette,
+ int *paletteEntries, bool *hasTransparency, int *colorType,
+ png_bytepp outRows) {
+ int w = imageInfo.width;
+ int h = imageInfo.height;
+ int i, j, rr, gg, bb, aa, idx;
+ uint32_t colors[256], col;
+ int num_colors = 0;
+ int maxGrayDeviation = 0;
+
+ bool isOpaque = true;
+ bool isPalette = true;
+ bool isGrayscale = true;
+
+ // 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
+
+ if (kDebug) {
+ printf("Initial image data:\n");
+ //dump_image(w, h, imageInfo.rows.data(), PNG_COLOR_TYPE_RGB_ALPHA);
+ }
+
+ for (j = 0; j < h; j++) {
+ const png_byte* row = imageInfo.rows[j];
+ png_bytep out = outRows[j];
+ for (i = 0; i < w; i++) {
+ rr = *row++;
+ gg = *row++;
+ bb = *row++;
+ aa = *row++;
+
+ int odev = maxGrayDeviation;
+ maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation);
+ maxGrayDeviation = MAX(ABS(gg - bb), maxGrayDeviation);
+ maxGrayDeviation = MAX(ABS(bb - rr), maxGrayDeviation);
+ if (maxGrayDeviation > odev) {
+ if (kDebug) {
+ printf("New max dev. = %d at pixel (%d, %d) = (%d %d %d %d)\n",
+ maxGrayDeviation, i, j, rr, gg, bb, aa);
+ }
+ }
+
+ // Check if image is really grayscale
+ if (isGrayscale) {
+ if (rr != gg || rr != bb) {
+ if (kDebug) {
+ printf("Found a non-gray pixel at %d, %d = (%d %d %d %d)\n",
+ i, j, rr, gg, bb, aa);
+ }
+ isGrayscale = false;
+ }
+ }
+
+ // Check if image is really opaque
+ if (isOpaque) {
+ if (aa != 0xff) {
+ if (kDebug) {
+ printf("Found a non-opaque pixel at %d, %d = (%d %d %d %d)\n",
+ i, j, rr, gg, bb, aa);
+ }
+ isOpaque = false;
+ }
+ }
+
+ // Check if image is really <= 256 colors
+ if (isPalette) {
+ col = (uint32_t) ((rr << 24) | (gg << 16) | (bb << 8) | aa);
+ bool match = false;
+ for (idx = 0; idx < num_colors; idx++) {
+ if (colors[idx] == col) {
+ 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
+ *out++ = idx;
+ if (!match) {
+ if (num_colors == 256) {
+ if (kDebug) {
+ printf("Found 257th color at %d, %d\n", i, j);
+ }
+ isPalette = false;
+ } else {
+ colors[num_colors++] = col;
+ }
+ }
+ }
+ }
+ }
+
+ *paletteEntries = 0;
+ *hasTransparency = !isOpaque;
+ int bpp = isOpaque ? 3 : 4;
+ int paletteSize = w * h + bpp * num_colors;
+
+ if (kDebug) {
+ printf("isGrayscale = %s\n", isGrayscale ? "true" : "false");
+ printf("isOpaque = %s\n", isOpaque ? "true" : "false");
+ printf("isPalette = %s\n", isPalette ? "true" : "false");
+ printf("Size w/ palette = %d, gray+alpha = %d, rgb(a) = %d\n",
+ paletteSize, 2 * w * h, bpp * w * h);
+ printf("Max gray deviation = %d, tolerance = %d\n", maxGrayDeviation, grayscaleTolerance);
+ }
+
+ // 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 = PNG_COLOR_TYPE_GRAY; // 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 * w * h)) {
+ *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color
+ } else {
+ *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel
+ }
+ }
+ } else if (isPalette && (paletteSize < bpp * w * h)) {
+ *colorType = PNG_COLOR_TYPE_PALETTE;
+ } else {
+ if (maxGrayDeviation <= grayscaleTolerance) {
+ logger->note() << "forcing image to gray (max deviation = " << maxGrayDeviation
+ << ")."
+ << std::endl;
+ *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA;
+ } else {
+ *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA;
+ }
+ }
+
+ // Perform postprocessing of the image or palette data based on the final
+ // color type chosen
+
+ if (*colorType == PNG_COLOR_TYPE_PALETTE) {
+ // Create separate RGB and Alpha palettes and set the number of colors
+ *paletteEntries = num_colors;
+
+ // Create the RGB and alpha palettes
+ for (int idx = 0; idx < num_colors; idx++) {
+ col = colors[idx];
+ rgbPalette[idx].red = (png_byte) ((col >> 24) & 0xff);
+ rgbPalette[idx].green = (png_byte) ((col >> 16) & 0xff);
+ rgbPalette[idx].blue = (png_byte) ((col >> 8) & 0xff);
+ alphaPalette[idx] = (png_byte) (col & 0xff);
+ }
+ } else if (*colorType == PNG_COLOR_TYPE_GRAY || *colorType == PNG_COLOR_TYPE_GRAY_ALPHA) {
+ // If the image is gray or gray + alpha, compact the pixels into outRows
+ for (j = 0; j < h; j++) {
+ const png_byte* row = imageInfo.rows[j];
+ png_bytep out = outRows[j];
+ for (i = 0; i < w; i++) {
+ rr = *row++;
+ gg = *row++;
+ bb = *row++;
+ aa = *row++;
+
+ if (isGrayscale) {
+ *out++ = rr;
+ } else {
+ *out++ = (png_byte) (rr * 0.2126f + gg * 0.7152f + bb * 0.0722f);
+ }
+ if (!isOpaque) {
+ *out++ = aa;
+ }
+ }
+ }
+ }
+}
+
+static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info,
+ int grayScaleTolerance, SourceLogger* logger, std::string* outError) {
+ if (setjmp(png_jmpbuf(writePtr))) {
+ *outError = "failed to write png";
+ return false;
+ }
+
+ uint32_t width, height;
+ int colorType, bitDepth, interlaceType, compressionType;
+
+ png_unknown_chunk unknowns[3];
+ unknowns[0].data = nullptr;
+ unknowns[1].data = nullptr;
+ unknowns[2].data = nullptr;
+
+ png_bytepp outRows = (png_bytepp) malloc((int) info->height * sizeof(png_bytep));
+ if (outRows == (png_bytepp) 0) {
+ printf("Can't allocate output buffer!\n");
+ exit(1);
+ }
+ for (uint32_t i = 0; i < info->height; i++) {
+ outRows[i] = (png_bytep) malloc(2 * (int) info->width);
+ if (outRows[i] == (png_bytep) 0) {
+ printf("Can't allocate output buffer!\n");
+ exit(1);
+ }
+ }
+
+ png_set_compression_level(writePtr, Z_BEST_COMPRESSION);
+
+ if (kDebug) {
+ logger->note() << "writing image: w = " << info->width
+ << ", h = " << info->height
+ << std::endl;
+ }
+
+ png_color rgbPalette[256];
+ png_byte alphaPalette[256];
+ bool hasTransparency;
+ int paletteEntries;
+
+ analyze_image(logger, *info, grayScaleTolerance, rgbPalette, alphaPalette,
+ &paletteEntries, &hasTransparency, &colorType, outRows);
+
+ // 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 (info->is9Patch && (colorType == PNG_COLOR_TYPE_RGB ||
+ colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_PALETTE)) {
+ colorType = PNG_COLOR_TYPE_RGB_ALPHA;
+ }
+
+ if (kDebug) {
+ switch (colorType) {
+ case PNG_COLOR_TYPE_PALETTE:
+ logger->note() << "has " << paletteEntries
+ << " colors" << (hasTransparency ? " (with alpha)" : "")
+ << ", using PNG_COLOR_TYPE_PALLETTE."
+ << std::endl;
+ break;
+ case PNG_COLOR_TYPE_GRAY:
+ logger->note() << "is opaque gray, using PNG_COLOR_TYPE_GRAY." << std::endl;
+ break;
+ case PNG_COLOR_TYPE_GRAY_ALPHA:
+ logger->note() << "is gray + alpha, using PNG_COLOR_TYPE_GRAY_ALPHA." << std::endl;
+ break;
+ case PNG_COLOR_TYPE_RGB:
+ logger->note() << "is opaque RGB, using PNG_COLOR_TYPE_RGB." << std::endl;
+ break;
+ case PNG_COLOR_TYPE_RGB_ALPHA:
+ logger->note() << "is RGB + alpha, using PNG_COLOR_TYPE_RGB_ALPHA." << std::endl;
+ break;
+ }
+ }
+
+ png_set_IHDR(writePtr, infoPtr, info->width, info->height, 8, colorType,
+ PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+
+ if (colorType == PNG_COLOR_TYPE_PALETTE) {
+ png_set_PLTE(writePtr, infoPtr, rgbPalette, paletteEntries);
+ if (hasTransparency) {
+ png_set_tRNS(writePtr, infoPtr, alphaPalette, paletteEntries, (png_color_16p) 0);
+ }
+ png_set_filter(writePtr, 0, PNG_NO_FILTERS);
+ } else {
+ png_set_filter(writePtr, 0, PNG_ALL_FILTERS);
+ }
+
+ if (info->is9Patch) {
+ int chunkCount = 2 + (info->haveLayoutBounds ? 1 : 0);
+ int pIndex = info->haveLayoutBounds ? 2 : 1;
+ int bIndex = 1;
+ int oIndex = 0;
+
+ // Chunks ordered thusly because older platforms depend on the base 9 patch data being last
+ png_bytep chunkNames = info->haveLayoutBounds
+ ? (png_bytep)"npOl\0npLb\0npTc\0"
+ : (png_bytep)"npOl\0npTc";
+
+ // base 9 patch data
+ if (kDebug) {
+ logger->note() << "adding 9-patch info..." << std::endl;
+ }
+ strcpy((char*)unknowns[pIndex].name, "npTc");
+ unknowns[pIndex].data = (png_byte*) info->serialize9Patch();
+ unknowns[pIndex].size = info->info9Patch.serializedSize();
+ // TODO: remove the check below when everything works
+ checkNinePatchSerialization(&info->info9Patch, unknowns[pIndex].data);
+
+ // automatically generated 9 patch outline data
+ int chunkSize = sizeof(png_uint_32) * 6;
+ strcpy((char*)unknowns[oIndex].name, "npOl");
+ unknowns[oIndex].data = (png_byte*) calloc(chunkSize, 1);
+ png_byte outputData[chunkSize];
+ memcpy(&outputData, &info->outlineInsetsLeft, 4 * sizeof(png_uint_32));
+ ((float*) outputData)[4] = info->outlineRadius;
+ ((png_uint_32*) outputData)[5] = info->outlineAlpha;
+ memcpy(unknowns[oIndex].data, &outputData, chunkSize);
+ unknowns[oIndex].size = chunkSize;
+
+ // optional optical inset / layout bounds data
+ if (info->haveLayoutBounds) {
+ int chunkSize = sizeof(png_uint_32) * 4;
+ strcpy((char*)unknowns[bIndex].name, "npLb");
+ unknowns[bIndex].data = (png_byte*) calloc(chunkSize, 1);
+ memcpy(unknowns[bIndex].data, &info->layoutBoundsLeft, chunkSize);
+ unknowns[bIndex].size = chunkSize;
+ }
+
+ for (int i = 0; i < chunkCount; i++) {
+ unknowns[i].location = PNG_HAVE_PLTE;
+ }
+ png_set_keep_unknown_chunks(writePtr, PNG_HANDLE_CHUNK_ALWAYS,
+ chunkNames, chunkCount);
+ png_set_unknown_chunks(writePtr, infoPtr, unknowns, chunkCount);
+
+#if PNG_LIBPNG_VER < 10600
+ // Deal with unknown chunk location bug in 1.5.x and earlier.
+ png_set_unknown_chunk_location(writePtr, infoPtr, 0, PNG_HAVE_PLTE);
+ if (info->haveLayoutBounds) {
+ png_set_unknown_chunk_location(writePtr, infoPtr, 1, PNG_HAVE_PLTE);
+ }
+#endif
+ }
+
+ png_write_info(writePtr, infoPtr);
+
+ png_bytepp rows;
+ if (colorType == PNG_COLOR_TYPE_RGB || colorType == PNG_COLOR_TYPE_RGB_ALPHA) {
+ if (colorType == PNG_COLOR_TYPE_RGB) {
+ png_set_filler(writePtr, 0, PNG_FILLER_AFTER);
+ }
+ rows = info->rows.data();
+ } else {
+ rows = outRows;
+ }
+ png_write_image(writePtr, rows);
+
+ if (kDebug) {
+ printf("Final image data:\n");
+ //dump_image(info->width, info->height, rows, colorType);
+ }
+
+ png_write_end(writePtr, infoPtr);
+
+ for (uint32_t i = 0; i < info->height; i++) {
+ free(outRows[i]);
+ }
+ free(outRows);
+ free(unknowns[0].data);
+ free(unknowns[1].data);
+ free(unknowns[2].data);
+
+ png_get_IHDR(writePtr, infoPtr, &width, &height, &bitDepth, &colorType, &interlaceType,
+ &compressionType, nullptr);
+
+ if (kDebug) {
+ logger->note() << "image written: w = " << width << ", h = " << height
+ << ", d = " << bitDepth << ", colors = " << colorType
+ << ", inter = " << interlaceType << ", comp = " << compressionType
+ << std::endl;
+ }
+ return true;
+}
+
+constexpr uint32_t kColorWhite = 0xffffffffu;
+constexpr uint32_t kColorTick = 0xff000000u;
+constexpr uint32_t kColorLayoutBoundsTick = 0xff0000ffu;
+
+enum class TickType {
+ kNone,
+ kTick,
+ kLayoutBounds,
+ kBoth
+};
+
+static TickType tickType(png_bytep p, bool transparent, const char** outError) {
+ png_uint_32 color = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24);
+
+ if (transparent) {
+ if (p[3] == 0) {
+ return TickType::kNone;
+ }
+ if (color == kColorLayoutBoundsTick) {
+ return TickType::kLayoutBounds;
+ }
+ if (color == kColorTick) {
+ return TickType::kTick;
+ }
+
+ // Error cases
+ if (p[3] != 0xff) {
+ *outError = "Frame pixels must be either solid or transparent "
+ "(not intermediate alphas)";
+ return TickType::kNone;
+ }
+
+ if (p[0] != 0 || p[1] != 0 || p[2] != 0) {
+ *outError = "Ticks in transparent frame must be black or red";
+ }
+ return TickType::kTick;
+ }
+
+ if (p[3] != 0xFF) {
+ *outError = "White frame must be a solid color (no alpha)";
+ }
+ if (color == kColorWhite) {
+ return TickType::kNone;
+ }
+ if (color == kColorTick) {
+ return TickType::kTick;
+ }
+ if (color == kColorLayoutBoundsTick) {
+ return TickType::kLayoutBounds;
+ }
+
+ if (p[0] != 0 || p[1] != 0 || p[2] != 0) {
+ *outError = "Ticks in white frame must be black or red";
+ return TickType::kNone;
+ }
+ return TickType::kTick;
+}
+
+enum class TickState {
+ kStart,
+ kInside1,
+ kOutside1
+};
+
+static bool getHorizontalTicks(png_bytep row, int width, bool transparent, bool required,
+ int32_t* outLeft, int32_t* outRight, const char** outError,
+ uint8_t* outDivs, bool multipleAllowed) {
+ *outLeft = *outRight = -1;
+ TickState state = TickState::kStart;
+ bool found = false;
+
+ for (int i = 1; i < width - 1; i++) {
+ if (tickType(row+i*4, transparent, outError) == TickType::kTick) {
+ if (state == TickState::kStart ||
+ (state == TickState::kOutside1 && multipleAllowed)) {
+ *outLeft = i-1;
+ *outRight = width-2;
+ found = true;
+ if (outDivs != NULL) {
+ *outDivs += 2;
+ }
+ state = TickState::kInside1;
+ } else if (state == TickState::kOutside1) {
+ *outError = "Can't have more than one marked region along edge";
+ *outLeft = i;
+ return false;
+ }
+ } else if (!*outError) {
+ if (state == TickState::kInside1) {
+ // We're done with this div. Move on to the next.
+ *outRight = i-1;
+ outRight += 2;
+ outLeft += 2;
+ state = TickState::kOutside1;
+ }
+ } else {
+ *outLeft = i;
+ return false;
+ }
+ }
+
+ if (required && !found) {
+ *outError = "No marked region found along edge";
+ *outLeft = -1;
+ return false;
+ }
+ return true;
+}
+
+static bool getVerticalTicks(png_bytepp rows, int offset, int height, bool transparent,
+ bool required, int32_t* outTop, int32_t* outBottom,
+ const char** outError, uint8_t* outDivs, bool multipleAllowed) {
+ *outTop = *outBottom = -1;
+ TickState state = TickState::kStart;
+ bool found = false;
+
+ for (int i = 1; i < height - 1; i++) {
+ if (tickType(rows[i]+offset, transparent, outError) == TickType::kTick) {
+ if (state == TickState::kStart ||
+ (state == TickState::kOutside1 && multipleAllowed)) {
+ *outTop = i-1;
+ *outBottom = height-2;
+ found = true;
+ if (outDivs != NULL) {
+ *outDivs += 2;
+ }
+ state = TickState::kInside1;
+ } else if (state == TickState::kOutside1) {
+ *outError = "Can't have more than one marked region along edge";
+ *outTop = i;
+ return false;
+ }
+ } else if (!*outError) {
+ if (state == TickState::kInside1) {
+ // We're done with this div. Move on to the next.
+ *outBottom = i-1;
+ outTop += 2;
+ outBottom += 2;
+ state = TickState::kOutside1;
+ }
+ } else {
+ *outTop = i;
+ return false;
+ }
+ }
+
+ if (required && !found) {
+ *outError = "No marked region found along edge";
+ *outTop = -1;
+ return false;
+ }
+ return true;
+}
+
+static bool getHorizontalLayoutBoundsTicks(png_bytep row, int width, bool transparent,
+ bool /* required */, int32_t* outLeft,
+ int32_t* outRight, const char** outError) {
+ *outLeft = *outRight = 0;
+
+ // Look for left tick
+ if (tickType(row + 4, transparent, outError) == TickType::kLayoutBounds) {
+ // Starting with a layout padding tick
+ int i = 1;
+ while (i < width - 1) {
+ (*outLeft)++;
+ i++;
+ if (tickType(row + i * 4, transparent, outError) != TickType::kLayoutBounds) {
+ break;
+ }
+ }
+ }
+
+ // Look for right tick
+ if (tickType(row + (width - 2) * 4, transparent, outError) == TickType::kLayoutBounds) {
+ // Ending with a layout padding tick
+ int i = width - 2;
+ while (i > 1) {
+ (*outRight)++;
+ i--;
+ if (tickType(row+i*4, transparent, outError) != TickType::kLayoutBounds) {
+ break;
+ }
+ }
+ }
+ return true;
+}
+
+static bool getVerticalLayoutBoundsTicks(png_bytepp rows, int offset, int height, bool transparent,
+ bool /* required */, int32_t* outTop, int32_t* outBottom,
+ const char** outError) {
+ *outTop = *outBottom = 0;
+
+ // Look for top tick
+ if (tickType(rows[1] + offset, transparent, outError) == TickType::kLayoutBounds) {
+ // Starting with a layout padding tick
+ int i = 1;
+ while (i < height - 1) {
+ (*outTop)++;
+ i++;
+ if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) {
+ break;
+ }
+ }
+ }
+
+ // Look for bottom tick
+ if (tickType(rows[height - 2] + offset, transparent, outError) == TickType::kLayoutBounds) {
+ // Ending with a layout padding tick
+ int i = height - 2;
+ while (i > 1) {
+ (*outBottom)++;
+ i--;
+ if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) {
+ break;
+ }
+ }
+ }
+ return true;
+}
+
+static void findMaxOpacity(png_bytepp rows, int startX, int startY, int endX, int endY,
+ int dX, int dY, int* outInset) {
+ uint8_t maxOpacity = 0;
+ int inset = 0;
+ *outInset = 0;
+ for (int x = startX, y = startY; x != endX && y != endY; x += dX, y += dY, inset++) {
+ png_byte* color = rows[y] + x * 4;
+ uint8_t opacity = color[3];
+ if (opacity > maxOpacity) {
+ maxOpacity = opacity;
+ *outInset = inset;
+ }
+ if (opacity == 0xff) return;
+ }
+}
+
+static uint8_t maxAlphaOverRow(png_bytep row, int startX, int endX) {
+ uint8_t maxAlpha = 0;
+ for (int x = startX; x < endX; x++) {
+ uint8_t alpha = (row + x * 4)[3];
+ if (alpha > maxAlpha) maxAlpha = alpha;
+ }
+ return maxAlpha;
+}
+
+static uint8_t maxAlphaOverCol(png_bytepp rows, int offsetX, int startY, int endY) {
+ uint8_t maxAlpha = 0;
+ for (int y = startY; y < endY; y++) {
+ uint8_t alpha = (rows[y] + offsetX * 4)[3];
+ if (alpha > maxAlpha) maxAlpha = alpha;
+ }
+ return maxAlpha;
+}
+
+static void getOutline(PngInfo* image) {
+ int midX = image->width / 2;
+ int midY = image->height / 2;
+ int endX = image->width - 2;
+ int endY = image->height - 2;
+
+ // find left and right extent of nine patch content on center row
+ if (image->width > 4) {
+ findMaxOpacity(image->rows.data(), 1, midY, midX, -1, 1, 0, &image->outlineInsetsLeft);
+ findMaxOpacity(image->rows.data(), endX, midY, midX, -1, -1, 0,
+ &image->outlineInsetsRight);
+ } else {
+ image->outlineInsetsLeft = 0;
+ image->outlineInsetsRight = 0;
+ }
+
+ // find top and bottom extent of nine patch content on center column
+ if (image->height > 4) {
+ findMaxOpacity(image->rows.data(), midX, 1, -1, midY, 0, 1, &image->outlineInsetsTop);
+ findMaxOpacity(image->rows.data(), midX, endY, -1, midY, 0, -1,
+ &image->outlineInsetsBottom);
+ } else {
+ image->outlineInsetsTop = 0;
+ image->outlineInsetsBottom = 0;
+ }
+
+ int innerStartX = 1 + image->outlineInsetsLeft;
+ int innerStartY = 1 + image->outlineInsetsTop;
+ int innerEndX = endX - image->outlineInsetsRight;
+ int innerEndY = endY - image->outlineInsetsBottom;
+ int innerMidX = (innerEndX + innerStartX) / 2;
+ int innerMidY = (innerEndY + innerStartY) / 2;
+
+ // assuming the image is a round rect, compute the radius by marching
+ // diagonally from the top left corner towards the center
+ image->outlineAlpha = std::max(
+ maxAlphaOverRow(image->rows[innerMidY], innerStartX, innerEndX),
+ maxAlphaOverCol(image->rows.data(), innerMidX, innerStartY, innerStartY));
+
+ int diagonalInset = 0;
+ findMaxOpacity(image->rows.data(), innerStartX, innerStartY, innerMidX, innerMidY, 1, 1,
+ &diagonalInset);
+
+ /* Determine source radius based upon inset:
+ * sqrt(r^2 + r^2) = sqrt(i^2 + i^2) + r
+ * sqrt(2) * r = sqrt(2) * i + r
+ * (sqrt(2) - 1) * r = sqrt(2) * i
+ * r = sqrt(2) / (sqrt(2) - 1) * i
+ */
+ image->outlineRadius = 3.4142f * diagonalInset;
+
+ if (kDebug) {
+ printf("outline insets %d %d %d %d, rad %f, alpha %x\n",
+ image->outlineInsetsLeft,
+ image->outlineInsetsTop,
+ image->outlineInsetsRight,
+ image->outlineInsetsBottom,
+ image->outlineRadius,
+ image->outlineAlpha);
+ }
+}
+
+static uint32_t getColor(png_bytepp rows, int left, int top, int right, int bottom) {
+ png_bytep color = rows[top] + left*4;
+
+ if (left > right || top > bottom) {
+ return android::Res_png_9patch::TRANSPARENT_COLOR;
+ }
+
+ while (top <= bottom) {
+ for (int i = left; i <= right; i++) {
+ png_bytep p = rows[top]+i*4;
+ if (color[3] == 0) {
+ if (p[3] != 0) {
+ return android::Res_png_9patch::NO_COLOR;
+ }
+ } else if (p[0] != color[0] || p[1] != color[1] ||
+ p[2] != color[2] || p[3] != color[3]) {
+ return android::Res_png_9patch::NO_COLOR;
+ }
+ }
+ top++;
+ }
+
+ if (color[3] == 0) {
+ return android::Res_png_9patch::TRANSPARENT_COLOR;
+ }
+ return (color[3]<<24) | (color[0]<<16) | (color[1]<<8) | color[2];
+}
+
+static bool do9Patch(PngInfo* image, std::string* outError) {
+ image->is9Patch = true;
+
+ int W = image->width;
+ int H = image->height;
+ int i, j;
+
+ const int maxSizeXDivs = W * sizeof(int32_t);
+ const int maxSizeYDivs = H * sizeof(int32_t);
+ int32_t* xDivs = image->xDivs = new int32_t[W];
+ int32_t* yDivs = image->yDivs = new int32_t[H];
+ uint8_t numXDivs = 0;
+ uint8_t numYDivs = 0;
+
+ int8_t numColors;
+ int numRows;
+ int numCols;
+ int top;
+ int left;
+ int right;
+ int bottom;
+ memset(xDivs, -1, maxSizeXDivs);
+ memset(yDivs, -1, maxSizeYDivs);
+ image->info9Patch.paddingLeft = image->info9Patch.paddingRight = -1;
+ image->info9Patch.paddingTop = image->info9Patch.paddingBottom = -1;
+ image->layoutBoundsLeft = image->layoutBoundsRight = 0;
+ image->layoutBoundsTop = image->layoutBoundsBottom = 0;
+
+ png_bytep p = image->rows[0];
+ bool transparent = p[3] == 0;
+ bool hasColor = false;
+
+ const char* errorMsg = nullptr;
+ int errorPixel = -1;
+ const char* errorEdge = nullptr;
+
+ int colorIndex = 0;
+ std::vector<png_bytep> newRows;
+
+ // Validate size...
+ if (W < 3 || H < 3) {
+ errorMsg = "Image must be at least 3x3 (1x1 without frame) pixels";
+ goto getout;
+ }
+
+ // Validate frame...
+ if (!transparent &&
+ (p[0] != 0xFF || p[1] != 0xFF || p[2] != 0xFF || p[3] != 0xFF)) {
+ errorMsg = "Must have one-pixel frame that is either transparent or white";
+ goto getout;
+ }
+
+ // Find left and right of sizing areas...
+ if (!getHorizontalTicks(p, W, transparent, true, &xDivs[0], &xDivs[1], &errorMsg, &numXDivs,
+ true)) {
+ errorPixel = xDivs[0];
+ errorEdge = "top";
+ goto getout;
+ }
+
+ // Find top and bottom of sizing areas...
+ if (!getVerticalTicks(image->rows.data(), 0, H, transparent, true, &yDivs[0], &yDivs[1],
+ &errorMsg, &numYDivs, true)) {
+ errorPixel = yDivs[0];
+ errorEdge = "left";
+ goto getout;
+ }
+
+ // Copy patch size data into image...
+ image->info9Patch.numXDivs = numXDivs;
+ image->info9Patch.numYDivs = numYDivs;
+
+ // Find left and right of padding area...
+ if (!getHorizontalTicks(image->rows[H-1], W, transparent, false,
+ &image->info9Patch.paddingLeft, &image->info9Patch.paddingRight,
+ &errorMsg, nullptr, false)) {
+ errorPixel = image->info9Patch.paddingLeft;
+ errorEdge = "bottom";
+ goto getout;
+ }
+
+ // Find top and bottom of padding area...
+ if (!getVerticalTicks(image->rows.data(), (W-1)*4, H, transparent, false,
+ &image->info9Patch.paddingTop, &image->info9Patch.paddingBottom,
+ &errorMsg, nullptr, false)) {
+ errorPixel = image->info9Patch.paddingTop;
+ errorEdge = "right";
+ goto getout;
+ }
+
+ // Find left and right of layout padding...
+ getHorizontalLayoutBoundsTicks(image->rows[H-1], W, transparent, false,
+ &image->layoutBoundsLeft, &image->layoutBoundsRight, &errorMsg);
+
+ getVerticalLayoutBoundsTicks(image->rows.data(), (W-1)*4, H, transparent, false,
+ &image->layoutBoundsTop, &image->layoutBoundsBottom, &errorMsg);
+
+ image->haveLayoutBounds = image->layoutBoundsLeft != 0
+ || image->layoutBoundsRight != 0
+ || image->layoutBoundsTop != 0
+ || image->layoutBoundsBottom != 0;
+
+ if (image->haveLayoutBounds) {
+ if (kDebug) {
+ printf("layoutBounds=%d %d %d %d\n", image->layoutBoundsLeft, image->layoutBoundsTop,
+ image->layoutBoundsRight, image->layoutBoundsBottom);
+ }
+ }
+
+ // use opacity of pixels to estimate the round rect outline
+ getOutline(image);
+
+ // If padding is not yet specified, take values from size.
+ if (image->info9Patch.paddingLeft < 0) {
+ image->info9Patch.paddingLeft = xDivs[0];
+ image->info9Patch.paddingRight = W - 2 - xDivs[1];
+ } else {
+ // Adjust value to be correct!
+ image->info9Patch.paddingRight = W - 2 - image->info9Patch.paddingRight;
+ }
+ if (image->info9Patch.paddingTop < 0) {
+ image->info9Patch.paddingTop = yDivs[0];
+ image->info9Patch.paddingBottom = H - 2 - yDivs[1];
+ } else {
+ // Adjust value to be correct!
+ image->info9Patch.paddingBottom = H - 2 - image->info9Patch.paddingBottom;
+ }
+
+/* if (kDebug) {
+ printf("Size ticks for %s: x0=%d, x1=%d, y0=%d, y1=%d\n", imageName,
+ xDivs[0], xDivs[1],
+ yDivs[0], yDivs[1]);
+ printf("padding ticks for %s: l=%d, r=%d, t=%d, b=%d\n", imageName,
+ image->info9Patch.paddingLeft, image->info9Patch.paddingRight,
+ image->info9Patch.paddingTop, image->info9Patch.paddingBottom);
+ }*/
+
+ // Remove frame from image.
+ newRows.resize(H - 2);
+ for (i = 0; i < H - 2; i++) {
+ newRows[i] = image->rows[i + 1];
+ memmove(newRows[i], newRows[i] + 4, (W - 2) * 4);
+ }
+ image->rows.swap(newRows);
+
+ image->width -= 2;
+ W = image->width;
+ image->height -= 2;
+ H = image->height;
+
+ // 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] == W) {
+ numCols--;
+ }
+ numRows = numYDivs + 1;
+ if (yDivs[0] == 0) { // Row 1 is strechable
+ numRows--;
+ }
+ if (yDivs[numYDivs - 1] == H) {
+ 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) {
+ errorMsg = "Too many rows and columns in 9-patch perimeter";
+ goto getout;
+ }
+
+ numColors = numRows * numCols;
+ image->info9Patch.numColors = numColors;
+ image->colors.resize(numColors);
+
+ // Fill in color information for each patch.
+
+ uint32_t 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 < H; j++) {
+ if (j == numYDivs) {
+ bottom = H;
+ } 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 < W; i++) {
+ if (i == numXDivs) {
+ right = W;
+ } else {
+ right = xDivs[i];
+ }
+ c = getColor(image->rows.data(), left, top, right - 1, bottom - 1);
+ image->colors[colorIndex++] = c;
+ if (kDebug) {
+ if (c != android::Res_png_9patch::NO_COLOR) {
+ hasColor = true;
+ }
+ }
+ left = right;
+ }
+ top = bottom;
+ }
+
+ assert(colorIndex == numColors);
+
+ if (kDebug && hasColor) {
+ for (i = 0; i < numColors; i++) {
+ if (i == 0) printf("Colors:\n");
+ printf(" #%08x", image->colors[i]);
+ if (i == numColors - 1) printf("\n");
+ }
+ }
+getout:
+ if (errorMsg) {
+ std::stringstream err;
+ err << "9-patch malformed: " << errorMsg;
+ if (!errorEdge) {
+ err << "." << std::endl;
+ if (errorPixel >= 0) {
+ err << "Found at pixel #" << errorPixel << " along " << errorEdge << " edge";
+ } else {
+ err << "Found along " << errorEdge << " edge";
+ }
+ }
+ *outError = err.str();
+ return false;
+ }
+ return true;
+}
+
+
+bool Png::process(const Source& source, std::istream& input, BigBuffer* outBuffer,
+ const Options& options, std::string* outError) {
+ png_byte signature[kPngSignatureSize];
+
+ // Read the PNG signature first.
+ if (!input.read(reinterpret_cast<char*>(signature), kPngSignatureSize)) {
+ *outError = strerror(errno);
+ return false;
+ }
+
+ // If the PNG signature doesn't match, bail early.
+ if (png_sig_cmp(signature, 0, kPngSignatureSize) != 0) {
+ *outError = "not a valid png file";
+ return false;
+ }
+
+ SourceLogger logger(source);
+ bool result = false;
+ png_structp readPtr = nullptr;
+ png_infop infoPtr = nullptr;
+ png_structp writePtr = nullptr;
+ png_infop writeInfoPtr = nullptr;
+ PngInfo pngInfo = {};
+
+ readPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr);
+ if (!readPtr) {
+ *outError = "failed to allocate read ptr";
+ goto bail;
+ }
+
+ infoPtr = png_create_info_struct(readPtr);
+ if (!infoPtr) {
+ *outError = "failed to allocate info ptr";
+ goto bail;
+ }
+
+ png_set_error_fn(readPtr, reinterpret_cast<png_voidp>(&logger), nullptr, logWarning);
+
+ // Set the read function to read from std::istream.
+ png_set_read_fn(readPtr, (png_voidp)&input, readDataFromStream);
+
+ if (!readPng(readPtr, infoPtr, &pngInfo, outError)) {
+ goto bail;
+ }
+
+ if (util::stringEndsWith<char>(source.path, ".9.png")) {
+ if (!do9Patch(&pngInfo, outError)) {
+ goto bail;
+ }
+ }
+
+ writePtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr);
+ if (!writePtr) {
+ *outError = "failed to allocate write ptr";
+ goto bail;
+ }
+
+ writeInfoPtr = png_create_info_struct(writePtr);
+ if (!writeInfoPtr) {
+ *outError = "failed to allocate write info ptr";
+ goto bail;
+ }
+
+ png_set_error_fn(writePtr, nullptr, nullptr, logWarning);
+
+ // Set the write function to write to std::ostream.
+ png_set_write_fn(writePtr, (png_voidp)outBuffer, writeDataToStream, flushDataToStream);
+
+ if (!writePng(writePtr, writeInfoPtr, &pngInfo, options.grayScaleTolerance, &logger,
+ outError)) {
+ goto bail;
+ }
+
+ result = true;
+bail:
+ if (readPtr) {
+ png_destroy_read_struct(&readPtr, &infoPtr, nullptr);
+ }
+
+ if (writePtr) {
+ png_destroy_write_struct(&writePtr, &writeInfoPtr);
+ }
+ return result;
+}
+
+} // namespace aapt