AAPT2: Refactor PngCrunching

PngCrunching now has a slightly better heuristic of choosing to encode
an image as a palette or RGB. For small images, RGB compresses much better
than a palette.

The original PNG is used as-is (minus some optional chunks being stripped)
if the resulting crunched PNG is larger than the original.

9-patch handling is abstracted away from PNGs, paving the way
for other 9-patches, like WebP.

TODO: handle PNGs with 9-patch chunks already present, which
should just be passed through. This will allow for 3rd party
tools to generate 9-patches.

TODO: implement cheap transparency: when one color is used to represent
transparent, and all other colors are opaque.

Bug:30053276
Change-Id: I5167f53b91d1efa462d9f03d6b9108d9b541c0c1
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk
index 9ec706f..c83a1328 100644
--- a/tools/aapt2/Android.mk
+++ b/tools/aapt2/Android.mk
@@ -24,7 +24,10 @@
 sources := \
 	compile/IdAssigner.cpp \
 	compile/InlineXmlFormatParser.cpp \
+	compile/NinePatch.cpp \
 	compile/Png.cpp \
+	compile/PngChunkFilter.cpp \
+	compile/PngCrunch.cpp \
 	compile/PseudolocaleGenerator.cpp \
 	compile/Pseudolocalizer.cpp \
 	compile/XmlIdCollector.cpp \
@@ -34,6 +37,7 @@
 	flatten/XmlFlattener.cpp \
 	io/File.cpp \
 	io/FileSystem.cpp \
+	io/Io.cpp \
 	io/ZipArchive.cpp \
 	link/AutoVersioner.cpp \
 	link/ManifestFixer.cpp \
@@ -80,6 +84,7 @@
 testSources := \
 	compile/IdAssigner_test.cpp \
 	compile/InlineXmlFormatParser_test.cpp \
+	compile/NinePatch_test.cpp \
 	compile/PseudolocaleGenerator_test.cpp \
 	compile/Pseudolocalizer_test.cpp \
 	compile/XmlIdCollector_test.cpp \
diff --git a/tools/aapt2/compile/Compile.cpp b/tools/aapt2/compile/Compile.cpp
index e0f37ec..dbd8062 100644
--- a/tools/aapt2/compile/Compile.cpp
+++ b/tools/aapt2/compile/Compile.cpp
@@ -36,6 +36,8 @@
 #include <google/protobuf/io/zero_copy_stream_impl_lite.h>
 #include <google/protobuf/io/coded_stream.h>
 
+#include <android-base/errors.h>
+#include <android-base/file.h>
 #include <dirent.h>
 #include <fstream>
 #include <string>
@@ -359,6 +361,9 @@
 static bool compileXml(IAaptContext* context, const CompileOptions& options,
                        const ResourcePathData& pathData, IArchiveWriter* writer,
                        const std::string& outputPath) {
+    if (context->verbose()) {
+        context->getDiagnostics()->note(DiagMessage(pathData.source) << "compiling XML");
+    }
 
     std::unique_ptr<xml::XmlResource> xmlRes;
     {
@@ -431,9 +436,43 @@
     return true;
 }
 
+class BigBufferOutputStream : public io::OutputStream {
+public:
+    explicit BigBufferOutputStream(BigBuffer* buffer) : mBuffer(buffer) {
+    }
+
+    bool Next(void** data, int* len) override {
+        size_t count;
+        *data = mBuffer->nextBlock(&count);
+        *len = static_cast<int>(count);
+        return true;
+    }
+
+    void BackUp(int count) override {
+        mBuffer->backUp(count);
+    }
+
+    int64_t ByteCount() const override {
+        return mBuffer->size();
+    }
+
+    bool HadError() const override {
+        return false;
+    }
+
+private:
+    BigBuffer* mBuffer;
+
+    DISALLOW_COPY_AND_ASSIGN(BigBufferOutputStream);
+};
+
 static bool compilePng(IAaptContext* context, const CompileOptions& options,
                        const ResourcePathData& pathData, IArchiveWriter* writer,
                        const std::string& outputPath) {
+    if (context->verbose()) {
+        context->getDiagnostics()->note(DiagMessage(pathData.source) << "compiling PNG");
+    }
+
     BigBuffer buffer(4096);
     ResourceFile resFile;
     resFile.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name);
@@ -441,16 +480,90 @@
     resFile.source = pathData.source;
 
     {
-        std::ifstream fin(pathData.source.path, std::ifstream::binary);
-        if (!fin) {
-            context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno));
+        std::string content;
+        if (!android::base::ReadFileToString(pathData.source.path, &content)) {
+            context->getDiagnostics()->error(DiagMessage(pathData.source)
+                                             << android::base::SystemErrorCodeToString(errno));
             return false;
         }
 
-        Png png(context->getDiagnostics());
-        if (!png.process(pathData.source, &fin, &buffer, {})) {
+        BigBuffer crunchedPngBuffer(4096);
+        BigBufferOutputStream crunchedPngBufferOut(&crunchedPngBuffer);
+
+        // Ensure that we only keep the chunks we care about if we end up
+        // using the original PNG instead of the crunched one.
+        PngChunkFilter pngChunkFilter(content);
+        std::unique_ptr<Image> image = readPng(context, &pngChunkFilter);
+        if (!image) {
             return false;
         }
+
+        std::unique_ptr<NinePatch> ninePatch;
+        if (pathData.extension == "9.png") {
+            std::string err;
+            ninePatch = NinePatch::create(image->rows.get(), image->width, image->height, &err);
+            if (!ninePatch) {
+                context->getDiagnostics()->error(DiagMessage() << err);
+                return false;
+            }
+
+            // Remove the 1px border around the NinePatch.
+            // Basically the row array is shifted up by 1, and the length is treated
+            // as height - 2.
+            // For each row, shift the array to the left by 1, and treat the length as width - 2.
+            image->width -= 2;
+            image->height -= 2;
+            memmove(image->rows.get(), image->rows.get() + 1, image->height * sizeof(uint8_t**));
+            for (int32_t h = 0; h < image->height; h++) {
+                memmove(image->rows[h], image->rows[h] + 4, image->width * 4);
+            }
+
+            if (context->verbose()) {
+                context->getDiagnostics()->note(DiagMessage(pathData.source)
+                                                << "9-patch: " << *ninePatch);
+            }
+        }
+
+        // Write the crunched PNG.
+        if (!writePng(context, image.get(), ninePatch.get(), &crunchedPngBufferOut, {})) {
+            return false;
+        }
+
+        if (ninePatch != nullptr
+                || crunchedPngBufferOut.ByteCount() <= pngChunkFilter.ByteCount()) {
+            // No matter what, we must use the re-encoded PNG, even if it is larger.
+            // 9-patch images must be re-encoded since their borders are stripped.
+            buffer.appendBuffer(std::move(crunchedPngBuffer));
+        } else {
+            // The re-encoded PNG is larger than the original, and there is
+            // no mandatory transformation. Use the original.
+            if (context->verbose()) {
+                context->getDiagnostics()->note(DiagMessage(pathData.source)
+                                                << "original PNG is smaller than crunched PNG"
+                                                << ", using original");
+            }
+
+            PngChunkFilter pngChunkFilterAgain(content);
+            BigBuffer filteredPngBuffer(4096);
+            BigBufferOutputStream filteredPngBufferOut(&filteredPngBuffer);
+            io::copy(&filteredPngBufferOut, &pngChunkFilterAgain);
+            buffer.appendBuffer(std::move(filteredPngBuffer));
+        }
+
+        if (context->verbose()) {
+            // For debugging only, use the legacy PNG cruncher and compare the resulting file sizes.
+            // This will help catch exotic cases where the new code may generate larger PNGs.
+            std::stringstream legacyStream(content);
+            BigBuffer legacyBuffer(4096);
+            Png png(context->getDiagnostics());
+            if (!png.process(pathData.source, &legacyStream, &legacyBuffer, {})) {
+                return false;
+            }
+
+            context->getDiagnostics()->note(DiagMessage(pathData.source)
+                                            << "legacy=" << legacyBuffer.size()
+                                            << " new=" << buffer.size());
+        }
     }
 
     if (!writeHeaderAndBufferToWriter(outputPath, resFile, buffer, writer,
@@ -463,6 +576,10 @@
 static bool compileFile(IAaptContext* context, const CompileOptions& options,
                         const ResourcePathData& pathData, IArchiveWriter* writer,
                         const std::string& outputPath) {
+    if (context->verbose()) {
+        context->getDiagnostics()->note(DiagMessage(pathData.source) << "compiling file");
+    }
+
     BigBuffer buffer(256);
     ResourceFile resFile;
     resFile.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name);
diff --git a/tools/aapt2/compile/Image.h b/tools/aapt2/compile/Image.h
new file mode 100644
index 0000000..fda6a3a
--- /dev/null
+++ b/tools/aapt2/compile/Image.h
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#ifndef AAPT_COMPILE_IMAGE_H
+#define AAPT_COMPILE_IMAGE_H
+
+#include <android-base/macros.h>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace aapt {
+
+/**
+ * An in-memory image, loaded from disk, with pixels in RGBA_8888 format.
+ */
+class Image {
+public:
+    explicit Image() = default;
+
+    /**
+     * A `height` sized array of pointers, where each element points to a
+     * `width` sized row of RGBA_8888 pixels.
+     */
+    std::unique_ptr<uint8_t*[]> rows;
+
+    /**
+     * The width of the image in RGBA_8888 pixels. This is int32_t because of 9-patch data
+     * format limitations.
+     */
+    int32_t width = 0;
+
+    /**
+     * The height of the image in RGBA_8888 pixels. This is int32_t because of 9-patch data
+     * format limitations.
+     */
+    int32_t height = 0;
+
+    /**
+     * Buffer to the raw image data stored sequentially.
+     * Use `rows` to access the data on a row-by-row basis.
+     */
+    std::unique_ptr<uint8_t[]> data;
+
+private:
+    DISALLOW_COPY_AND_ASSIGN(Image);
+};
+
+/**
+ * A range of pixel values, starting at 'start' and ending before 'end' exclusive. Or rather [a, b).
+ */
+struct Range {
+    int32_t start = 0;
+    int32_t end = 0;
+
+    explicit Range() = default;
+    inline explicit Range(int32_t s, int32_t e) : start(s), end(e) {
+    }
+};
+
+inline bool operator==(const Range& left, const Range& right) {
+    return left.start == right.start && left.end == right.end;
+}
+
+/**
+ * Inset lengths from all edges of a rectangle. `left` and `top` are measured from the left and top
+ * edges, while `right` and `bottom` are measured from the right and bottom edges, respectively.
+ */
+struct Bounds {
+    int32_t left = 0;
+    int32_t top = 0;
+    int32_t right = 0;
+    int32_t bottom = 0;
+
+    explicit Bounds() = default;
+    inline explicit Bounds(int32_t l, int32_t t, int32_t r, int32_t b) :
+            left(l), top(t), right(r), bottom(b) {
+    }
+
+    bool nonZero() const;
+};
+
+inline bool Bounds::nonZero() const {
+    return left != 0 || top != 0 || right != 0 || bottom != 0;
+}
+
+inline bool operator==(const Bounds& left, const Bounds& right) {
+    return left.left == right.left && left.top == right.top &&
+            left.right == right.right && left.bottom == right.bottom;
+}
+
+/**
+ * Contains 9-patch data from a source image. All measurements exclude the 1px border of the
+ * source 9-patch image.
+ */
+class NinePatch {
+public:
+    static std::unique_ptr<NinePatch> create(uint8_t** rows,
+                                             const int32_t width, const int32_t height,
+                                             std::string* errOut);
+
+    /**
+     * Packs the RGBA_8888 data pointed to by pixel into a uint32_t
+     * with format 0xAARRGGBB (the way 9-patch expects it).
+     */
+    static uint32_t packRGBA(const uint8_t* pixel);
+
+    /**
+     * 9-patch content padding/insets. All positions are relative to the 9-patch
+     * NOT including the 1px thick source border.
+     */
+    Bounds padding;
+
+    /**
+     * Optical layout bounds/insets. This overrides the padding for
+     * layout purposes. All positions are relative to the 9-patch
+     * NOT including the 1px thick source border.
+     * See https://developer.android.com/about/versions/android-4.3.html#OpticalBounds
+     */
+    Bounds layoutBounds;
+
+    /**
+     * Outline of the image, calculated based on opacity.
+     */
+    Bounds outline;
+
+    /**
+     * The computed radius of the outline. If non-zero, the outline is a rounded-rect.
+     */
+    float outlineRadius = 0.0f;
+
+    /**
+     * The largest alpha value within the outline.
+     */
+    uint32_t outlineAlpha = 0x000000ffu;
+
+    /**
+     * Horizontal regions of the image that are stretchable.
+     * All positions are relative to the 9-patch
+     * NOT including the 1px thick source border.
+     */
+    std::vector<Range> horizontalStretchRegions;
+
+    /**
+     * Vertical regions of the image that are stretchable.
+     * All positions are relative to the 9-patch
+     * NOT including the 1px thick source border.
+     */
+    std::vector<Range> verticalStretchRegions;
+
+    /**
+     * The colors within each region, fixed or stretchable.
+     * For w*h regions, the color of region (x,y) is addressable
+     * via index y*w + x.
+     */
+    std::vector<uint32_t> regionColors;
+
+    /**
+     * Returns serialized data containing the original basic 9-patch meta data.
+     * Optical layout bounds and round rect outline data must be serialized
+     * separately using serializeOpticalLayoutBounds() and serializeRoundedRectOutline().
+     */
+    std::unique_ptr<uint8_t[]> serializeBase(size_t* outLen) const;
+
+    /**
+     * Serializes the layout bounds.
+     */
+    std::unique_ptr<uint8_t[]> serializeLayoutBounds(size_t* outLen) const;
+
+    /**
+     * Serializes the rounded-rect outline.
+     */
+    std::unique_ptr<uint8_t[]> serializeRoundedRectOutline(size_t* outLen) const;
+
+private:
+    explicit NinePatch() = default;
+
+    DISALLOW_COPY_AND_ASSIGN(NinePatch);
+};
+
+::std::ostream& operator<<(::std::ostream& out, const Range& range);
+::std::ostream& operator<<(::std::ostream& out, const Bounds& bounds);
+::std::ostream& operator<<(::std::ostream& out, const NinePatch& ninePatch);
+
+} // namespace aapt
+
+#endif /* AAPT_COMPILE_IMAGE_H */
diff --git a/tools/aapt2/compile/NinePatch.cpp b/tools/aapt2/compile/NinePatch.cpp
new file mode 100644
index 0000000..408ecf7
--- /dev/null
+++ b/tools/aapt2/compile/NinePatch.cpp
@@ -0,0 +1,671 @@
+/*
+ * Copyright (C) 2016 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 "compile/Image.h"
+#include "util/StringPiece.h"
+#include "util/Util.h"
+
+#include <androidfw/ResourceTypes.h>
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace aapt {
+
+// Colors in the format 0xAARRGGBB (the way 9-patch expects it).
+constexpr static const uint32_t kColorOpaqueWhite = 0xffffffffu;
+constexpr static const uint32_t kColorOpaqueBlack = 0xff000000u;
+constexpr static const uint32_t kColorOpaqueRed   = 0xffff0000u;
+
+constexpr static const uint32_t kPrimaryColor = kColorOpaqueBlack;
+constexpr static const uint32_t kSecondaryColor = kColorOpaqueRed;
+
+/**
+ * Returns the alpha value encoded in the 0xAARRGBB encoded pixel.
+ */
+static uint32_t getAlpha(uint32_t color);
+
+/**
+ * Determines whether a color on an ImageLine is valid.
+ * A 9patch image may use a transparent color as neutral,
+ * or a fully opaque white color as neutral, based on the
+ * pixel color at (0,0) of the image. One or the other is fine,
+ * but we need to ensure consistency throughout the image.
+ */
+class ColorValidator {
+public:
+    virtual ~ColorValidator() = default;
+
+    /**
+     * Returns true if the color specified is a neutral color
+     * (no padding, stretching, or optical bounds).
+     */
+    virtual bool isNeutralColor(uint32_t color) const = 0;
+
+    /**
+     * Returns true if the color is either a neutral color
+     * or one denoting padding, stretching, or optical bounds.
+     */
+    bool isValidColor(uint32_t color) const {
+        switch (color) {
+        case kPrimaryColor:
+        case kSecondaryColor:
+            return true;
+        }
+        return isNeutralColor(color);
+    }
+};
+
+// Walks an ImageLine and records Ranges of primary and secondary colors.
+// The primary color is black and is used to denote a padding or stretching range,
+// depending on which border we're iterating over.
+// The secondary color is red and is used to denote optical bounds.
+//
+// An ImageLine is a templated-interface that would look something like this if it
+// were polymorphic:
+//
+// class ImageLine {
+// public:
+//      virtual int32_t getLength() const = 0;
+//      virtual uint32_t getColor(int32_t idx) const = 0;
+// };
+//
+template <typename ImageLine>
+static bool fillRanges(const ImageLine* imageLine,
+                       const ColorValidator* colorValidator,
+                       std::vector<Range>* primaryRanges,
+                       std::vector<Range>* secondaryRanges,
+                       std::string* err) {
+    const int32_t length = imageLine->getLength();
+
+    uint32_t lastColor = 0xffffffffu;
+    for (int32_t idx = 1; idx < length - 1; idx++) {
+        const uint32_t color = imageLine->getColor(idx);
+        if (!colorValidator->isValidColor(color)) {
+            *err = "found an invalid color";
+            return false;
+        }
+
+        if (color != lastColor) {
+            // We are ending a range. Which range?
+            // note: encode the x offset without the final 1 pixel border.
+            if (lastColor == kPrimaryColor) {
+                primaryRanges->back().end = idx - 1;
+            } else if (lastColor == kSecondaryColor) {
+                secondaryRanges->back().end = idx - 1;
+            }
+
+            // We are starting a range. Which range?
+            // note: encode the x offset without the final 1 pixel border.
+            if (color == kPrimaryColor) {
+                primaryRanges->push_back(Range(idx - 1, length - 2));
+            } else if (color == kSecondaryColor) {
+                secondaryRanges->push_back(Range(idx - 1, length - 2));
+            }
+            lastColor = color;
+        }
+    }
+    return true;
+}
+
+/**
+ * Iterates over a row in an image. Implements the templated ImageLine interface.
+ */
+class HorizontalImageLine {
+public:
+    explicit HorizontalImageLine(uint8_t** rows, int32_t xOffset, int32_t yOffset,
+                                 int32_t length) :
+            mRows(rows), mXOffset(xOffset), mYOffset(yOffset), mLength(length) {
+    }
+
+    inline int32_t getLength() const {
+        return mLength;
+    }
+
+    inline uint32_t getColor(int32_t idx) const {
+        return NinePatch::packRGBA(mRows[mYOffset] + (idx + mXOffset) * 4);
+    }
+
+private:
+    uint8_t** mRows;
+    int32_t mXOffset, mYOffset, mLength;
+
+    DISALLOW_COPY_AND_ASSIGN(HorizontalImageLine);
+};
+
+/**
+ * Iterates over a column in an image. Implements the templated ImageLine interface.
+ */
+class VerticalImageLine {
+public:
+    explicit VerticalImageLine(uint8_t** rows, int32_t xOffset, int32_t yOffset,
+                               int32_t length) :
+            mRows(rows), mXOffset(xOffset), mYOffset(yOffset), mLength(length) {
+    }
+
+    inline int32_t getLength() const {
+        return mLength;
+    }
+
+    inline uint32_t getColor(int32_t idx) const {
+        return NinePatch::packRGBA(mRows[mYOffset + idx] + (mXOffset * 4));
+    }
+
+private:
+    uint8_t** mRows;
+    int32_t mXOffset, mYOffset, mLength;
+
+    DISALLOW_COPY_AND_ASSIGN(VerticalImageLine);
+};
+
+class DiagonalImageLine {
+public:
+    explicit DiagonalImageLine(uint8_t** rows, int32_t xOffset, int32_t yOffset,
+                               int32_t xStep, int32_t yStep, int32_t length) :
+            mRows(rows), mXOffset(xOffset), mYOffset(yOffset), mXStep(xStep), mYStep(yStep),
+            mLength(length) {
+    }
+
+    inline int32_t getLength() const {
+        return mLength;
+    }
+
+    inline uint32_t getColor(int32_t idx) const {
+        return NinePatch::packRGBA(
+                mRows[mYOffset + (idx * mYStep)] + ((idx + mXOffset) * mXStep) * 4);
+    }
+
+private:
+    uint8_t** mRows;
+    int32_t mXOffset, mYOffset, mXStep, mYStep, mLength;
+
+    DISALLOW_COPY_AND_ASSIGN(DiagonalImageLine);
+};
+
+class TransparentNeutralColorValidator : public ColorValidator {
+public:
+    bool isNeutralColor(uint32_t color) const override {
+        return getAlpha(color) == 0;
+    }
+};
+
+class WhiteNeutralColorValidator : public ColorValidator {
+public:
+    bool isNeutralColor(uint32_t color) const override {
+        return color == kColorOpaqueWhite;
+    }
+};
+
+inline static uint32_t getAlpha(uint32_t color) {
+    return (color & 0xff000000u) >> 24;
+}
+
+static bool populateBounds(const std::vector<Range>& padding,
+                           const std::vector<Range>& layoutBounds,
+                           const std::vector<Range>& stretchRegions,
+                           const int32_t length,
+                           int32_t* paddingStart, int32_t* paddingEnd,
+                           int32_t* layoutStart, int32_t* layoutEnd,
+                           const StringPiece& edgeName,
+                           std::string* err) {
+    if (padding.size() > 1) {
+        std::stringstream errStream;
+        errStream << "too many padding sections on " << edgeName << " border";
+        *err = errStream.str();
+        return false;
+    }
+
+    *paddingStart = 0;
+    *paddingEnd = 0;
+    if (!padding.empty()) {
+        const Range& range = padding.front();
+        *paddingStart = range.start;
+        *paddingEnd = length - range.end;
+    } else if (!stretchRegions.empty()) {
+        // No padding was defined. Compute the padding from the first and last
+        // stretch regions.
+        *paddingStart = stretchRegions.front().start;
+        *paddingEnd = length - stretchRegions.back().end;
+    }
+
+    if (layoutBounds.size() > 2) {
+        std::stringstream errStream;
+        errStream << "too many layout bounds sections on " << edgeName << " border";
+        *err = errStream.str();
+        return false;
+    }
+
+    *layoutStart = 0;
+    *layoutEnd = 0;
+    if (layoutBounds.size() >= 1) {
+        const Range& range = layoutBounds.front();
+        // If there is only one layout bound segment, it might not start at 0, but then it should
+        // end at length.
+        if (range.start != 0 && range.end != length) {
+            std::stringstream errStream;
+            errStream << "layout bounds on " << edgeName << " border must start at edge";
+            *err = errStream.str();
+            return false;
+        }
+        *layoutStart = range.end;
+
+        if (layoutBounds.size() >= 2) {
+            const Range& range = layoutBounds.back();
+            if (range.end != length) {
+                std::stringstream errStream;
+                errStream << "layout bounds on " << edgeName << " border must start at edge";
+                *err = errStream.str();
+                return false;
+            }
+            *layoutEnd = length - range.start;
+        }
+    }
+    return true;
+}
+
+static int32_t calculateSegmentCount(const std::vector<Range>& stretchRegions, int32_t length) {
+    if (stretchRegions.size() == 0) {
+        return 0;
+    }
+
+    const bool startIsFixed = stretchRegions.front().start != 0;
+    const bool endIsFixed = stretchRegions.back().end != length;
+    int32_t modifier = 0;
+    if (startIsFixed && endIsFixed) {
+        modifier = 1;
+    } else if (!startIsFixed && !endIsFixed) {
+        modifier = -1;
+    }
+    return static_cast<int32_t>(stretchRegions.size()) * 2 + modifier;
+}
+
+static uint32_t getRegionColor(uint8_t** rows, const Bounds& region) {
+    // Sample the first pixel to compare against.
+    const uint32_t expectedColor = NinePatch::packRGBA(rows[region.top] + region.left * 4);
+    for (int32_t y = region.top; y < region.bottom; y++) {
+        const uint8_t* row = rows[y];
+        for (int32_t x = region.left; x < region.right; x++) {
+            const uint32_t color = NinePatch::packRGBA(row + x * 4);
+            if (getAlpha(color) == 0) {
+                // The color is transparent.
+                // If the expectedColor is not transparent, NO_COLOR.
+                if (getAlpha(expectedColor) != 0) {
+                    return android::Res_png_9patch::NO_COLOR;
+                }
+            } else if (color != expectedColor) {
+                return android::Res_png_9patch::NO_COLOR;
+            }
+        }
+    }
+
+    if (getAlpha(expectedColor) == 0) {
+        return android::Res_png_9patch::TRANSPARENT_COLOR;
+    }
+    return expectedColor;
+}
+
+// Fills outColors with each 9-patch section's colour. If the whole section is transparent,
+// it gets the special TRANSPARENT colour. If the whole section is the same colour, it is assigned
+// that colour. Otherwise it gets the special NO_COLOR colour.
+//
+// Note that the rows contain the 9-patch 1px border, and the indices in the stretch regions are
+// already offset to exclude the border. This means that each time the rows are accessed,
+// the indices must be offset by 1.
+//
+// width and height also include the 9-patch 1px border.
+static void calculateRegionColors(uint8_t** rows,
+                                  const std::vector<Range>& horizontalStretchRegions,
+                                  const std::vector<Range>& verticalStretchRegions,
+                                  const int32_t width, const int32_t height,
+                                  std::vector<uint32_t>* outColors) {
+    int32_t nextTop = 0;
+    Bounds bounds;
+    auto rowIter = verticalStretchRegions.begin();
+    while (nextTop != height) {
+        if (rowIter != verticalStretchRegions.end()) {
+            if (nextTop != rowIter->start) {
+                // This is a fixed segment.
+                // Offset the bounds by 1 to accommodate the border.
+                bounds.top = nextTop + 1;
+                bounds.bottom = rowIter->start + 1;
+                nextTop = rowIter->start;
+            } else {
+                // This is a stretchy segment.
+                // Offset the bounds by 1 to accommodate the border.
+                bounds.top = rowIter->start + 1;
+                bounds.bottom = rowIter->end + 1;
+                nextTop = rowIter->end;
+                ++rowIter;
+            }
+        } else {
+            // This is the end, fixed section.
+            // Offset the bounds by 1 to accommodate the border.
+            bounds.top = nextTop + 1;
+            bounds.bottom = height + 1;
+            nextTop = height;
+         }
+
+        int32_t nextLeft = 0;
+        auto colIter = horizontalStretchRegions.begin();
+        while (nextLeft != width) {
+            if (colIter != horizontalStretchRegions.end()) {
+                if (nextLeft != colIter->start) {
+                    // This is a fixed segment.
+                    // Offset the bounds by 1 to accommodate the border.
+                    bounds.left = nextLeft + 1;
+                    bounds.right = colIter->start + 1;
+                    nextLeft = colIter->start;
+                } else {
+                    // This is a stretchy segment.
+                    // Offset the bounds by 1 to accommodate the border.
+                    bounds.left = colIter->start + 1;
+                    bounds.right = colIter->end + 1;
+                    nextLeft = colIter->end;
+                    ++colIter;
+                }
+            } else {
+                // This is the end, fixed section.
+                // Offset the bounds by 1 to accommodate the border.
+                bounds.left = nextLeft + 1;
+                bounds.right = width + 1;
+                nextLeft = width;
+            }
+            outColors->push_back(getRegionColor(rows, bounds));
+        }
+    }
+}
+
+// Calculates the insets of a row/column of pixels based on where the largest alpha value begins
+// (on both sides).
+template <typename ImageLine>
+static void findOutlineInsets(const ImageLine* imageLine, int32_t* outStart, int32_t* outEnd) {
+    *outStart = 0;
+    *outEnd = 0;
+
+    const int32_t length = imageLine->getLength();
+    if (length < 3) {
+        return;
+    }
+
+    // If the length is odd, we want both sides to process the center pixel,
+    // so we use two different midpoints (to account for < and <= in the different loops).
+    const int32_t mid2 = length / 2;
+    const int32_t mid1 = mid2 + (length % 2);
+
+    uint32_t maxAlpha = 0;
+    for (int32_t i = 0; i < mid1 && maxAlpha != 0xff; i++) {
+        uint32_t alpha = getAlpha(imageLine->getColor(i));
+        if (alpha > maxAlpha) {
+            maxAlpha = alpha;
+            *outStart = i;
+        }
+    }
+
+    maxAlpha = 0;
+    for (int32_t i = length - 1; i >= mid2 && maxAlpha != 0xff; i--) {
+        uint32_t alpha = getAlpha(imageLine->getColor(i));
+        if (alpha > maxAlpha) {
+            maxAlpha = alpha;
+            *outEnd = length - (i + 1);
+        }
+    }
+    return;
+}
+
+template <typename ImageLine>
+static uint32_t findMaxAlpha(const ImageLine* imageLine) {
+    const int32_t length = imageLine->getLength();
+    uint32_t maxAlpha = 0;
+    for (int32_t idx = 0; idx < length && maxAlpha != 0xff; idx++) {
+        uint32_t alpha = getAlpha(imageLine->getColor(idx));
+        if (alpha > maxAlpha) {
+            maxAlpha = alpha;
+        }
+    }
+    return maxAlpha;
+}
+
+// Pack the pixels in as 0xAARRGGBB (as 9-patch expects it).
+uint32_t NinePatch::packRGBA(const uint8_t* pixel) {
+    return (pixel[3] << 24) | (pixel[0] << 16) | (pixel[1] << 8) | pixel[2];
+}
+
+std::unique_ptr<NinePatch> NinePatch::create(uint8_t** rows,
+                                             const int32_t width, const int32_t height,
+                                             std::string* err) {
+    if (width < 3 || height < 3) {
+        *err = "image must be at least 3x3 (1x1 image with 1 pixel border)";
+        return {};
+    }
+
+    std::vector<Range> horizontalPadding;
+    std::vector<Range> horizontalOpticalBounds;
+    std::vector<Range> verticalPadding;
+    std::vector<Range> verticalOpticalBounds;
+    std::vector<Range> unexpectedRanges;
+    std::unique_ptr<ColorValidator> colorValidator;
+
+    if (rows[0][3] == 0) {
+        colorValidator = util::make_unique<TransparentNeutralColorValidator>();
+    } else if (packRGBA(rows[0]) == kColorOpaqueWhite) {
+        colorValidator = util::make_unique<WhiteNeutralColorValidator>();
+    } else {
+        *err = "top-left corner pixel must be either opaque white or transparent";
+        return {};
+    }
+
+    // Private constructor, can't use make_unique.
+    auto ninePatch = std::unique_ptr<NinePatch>(new NinePatch());
+
+    HorizontalImageLine topRow(rows, 0, 0, width);
+    if (!fillRanges(&topRow, colorValidator.get(), &ninePatch->horizontalStretchRegions,
+                    &unexpectedRanges, err)) {
+        return {};
+    }
+
+    if (!unexpectedRanges.empty()) {
+        const Range& range = unexpectedRanges[0];
+        std::stringstream errStream;
+        errStream << "found unexpected optical bounds (red pixel) on top border "
+                << "at x=" << range.start + 1;
+        *err = errStream.str();
+        return {};
+    }
+
+    VerticalImageLine leftCol(rows, 0, 0, height);
+    if (!fillRanges(&leftCol, colorValidator.get(), &ninePatch->verticalStretchRegions,
+                    &unexpectedRanges, err)) {
+        return {};
+    }
+
+    if (!unexpectedRanges.empty()) {
+        const Range& range = unexpectedRanges[0];
+        std::stringstream errStream;
+        errStream << "found unexpected optical bounds (red pixel) on left border "
+                << "at y=" << range.start + 1;
+        return {};
+    }
+
+    HorizontalImageLine bottomRow(rows, 0, height - 1, width);
+    if (!fillRanges(&bottomRow, colorValidator.get(), &horizontalPadding,
+                    &horizontalOpticalBounds, err)) {
+        return {};
+    }
+
+    if (!populateBounds(horizontalPadding, horizontalOpticalBounds,
+                        ninePatch->horizontalStretchRegions, width - 2,
+                        &ninePatch->padding.left, &ninePatch->padding.right,
+                        &ninePatch->layoutBounds.left, &ninePatch->layoutBounds.right,
+                        "bottom", err)) {
+        return {};
+    }
+
+    VerticalImageLine rightCol(rows, width - 1, 0, height);
+    if (!fillRanges(&rightCol, colorValidator.get(), &verticalPadding,
+                    &verticalOpticalBounds, err)) {
+        return {};
+    }
+
+    if (!populateBounds(verticalPadding, verticalOpticalBounds,
+                        ninePatch->verticalStretchRegions, height - 2,
+                        &ninePatch->padding.top, &ninePatch->padding.bottom,
+                        &ninePatch->layoutBounds.top, &ninePatch->layoutBounds.bottom,
+                        "right", err)) {
+        return {};
+    }
+
+    // Fill the region colors of the 9-patch.
+    const int32_t numRows = calculateSegmentCount(ninePatch->horizontalStretchRegions, width - 2);
+    const int32_t numCols = calculateSegmentCount(ninePatch->verticalStretchRegions, height - 2);
+    if ((int64_t) numRows * (int64_t) numCols > 0x7f) {
+        *err = "too many regions in 9-patch";
+        return {};
+    }
+
+    ninePatch->regionColors.reserve(numRows * numCols);
+    calculateRegionColors(rows, ninePatch->horizontalStretchRegions,
+                          ninePatch->verticalStretchRegions,
+                          width - 2, height - 2,
+                          &ninePatch->regionColors);
+
+    // Compute the outline based on opacity.
+
+    // Find left and right extent of 9-patch content on center row.
+    HorizontalImageLine midRow(rows, 1, height / 2, width - 2);
+    findOutlineInsets(&midRow, &ninePatch->outline.left, &ninePatch->outline.right);
+
+    // Find top and bottom extent of 9-patch content on center column.
+    VerticalImageLine midCol(rows, width / 2, 1, height - 2);
+    findOutlineInsets(&midCol, &ninePatch->outline.top, &ninePatch->outline.bottom);
+
+    const int32_t outlineWidth = (width - 2) - ninePatch->outline.left - ninePatch->outline.right;
+    const int32_t outlineHeight = (height - 2) - ninePatch->outline.top - ninePatch->outline.bottom;
+
+    // Find the largest alpha value within the outline area.
+    HorizontalImageLine outlineMidRow(rows,
+                                      1 + ninePatch->outline.left,
+                                      1 + ninePatch->outline.top + (outlineHeight / 2),
+                                      outlineWidth);
+    VerticalImageLine outlineMidCol(rows,
+                                    1 + ninePatch->outline.left + (outlineWidth / 2),
+                                    1 + ninePatch->outline.top,
+                                    outlineHeight);
+    ninePatch->outlineAlpha = std::max(findMaxAlpha(&outlineMidRow), findMaxAlpha(&outlineMidCol));
+
+    // Assuming the image is a round rect, compute the radius by marching
+    // diagonally from the top left corner towards the center.
+    DiagonalImageLine diagonal(rows, 1 + ninePatch->outline.left, 1 + ninePatch->outline.top,
+                               1, 1, std::min(outlineWidth, outlineHeight));
+    int32_t topLeft, bottomRight;
+    findOutlineInsets(&diagonal, &topLeft, &bottomRight);
+
+    /* 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
+     */
+    ninePatch->outlineRadius = 3.4142f * topLeft;
+    return ninePatch;
+}
+
+std::unique_ptr<uint8_t[]> NinePatch::serializeBase(size_t* outLen) const {
+    android::Res_png_9patch data;
+    data.numXDivs = static_cast<uint8_t>(horizontalStretchRegions.size()) * 2;
+    data.numYDivs = static_cast<uint8_t>(verticalStretchRegions.size()) * 2;
+    data.numColors = static_cast<uint8_t>(regionColors.size());
+    data.paddingLeft = padding.left;
+    data.paddingRight = padding.right;
+    data.paddingTop = padding.top;
+    data.paddingBottom = padding.bottom;
+
+    auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[data.serializedSize()]);
+    android::Res_png_9patch::serialize(data,
+                                       (const int32_t*) horizontalStretchRegions.data(),
+                                       (const int32_t*) verticalStretchRegions.data(),
+                                       regionColors.data(),
+                                       buffer.get());
+    *outLen = data.serializedSize();
+    return buffer;
+}
+
+std::unique_ptr<uint8_t[]> NinePatch::serializeLayoutBounds(size_t* outLen) const {
+    size_t chunkLen = sizeof(uint32_t) * 4;
+    auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[chunkLen]);
+    uint8_t* cursor = buffer.get();
+
+    memcpy(cursor, &layoutBounds.left, sizeof(layoutBounds.left));
+    cursor += sizeof(layoutBounds.left);
+
+    memcpy(cursor, &layoutBounds.top, sizeof(layoutBounds.top));
+    cursor += sizeof(layoutBounds.top);
+
+    memcpy(cursor, &layoutBounds.right, sizeof(layoutBounds.right));
+    cursor += sizeof(layoutBounds.right);
+
+    memcpy(cursor, &layoutBounds.bottom, sizeof(layoutBounds.bottom));
+    cursor += sizeof(layoutBounds.bottom);
+
+    *outLen = chunkLen;
+    return buffer;
+}
+
+std::unique_ptr<uint8_t[]> NinePatch::serializeRoundedRectOutline(size_t* outLen) const {
+    size_t chunkLen = sizeof(uint32_t) * 6;
+    auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[chunkLen]);
+    uint8_t* cursor = buffer.get();
+
+    memcpy(cursor, &outline.left, sizeof(outline.left));
+    cursor += sizeof(outline.left);
+
+    memcpy(cursor, &outline.top, sizeof(outline.top));
+    cursor += sizeof(outline.top);
+
+    memcpy(cursor, &outline.right, sizeof(outline.right));
+    cursor += sizeof(outline.right);
+
+    memcpy(cursor, &outline.bottom, sizeof(outline.bottom));
+    cursor += sizeof(outline.bottom);
+
+    *((float*) cursor) = outlineRadius;
+    cursor += sizeof(outlineRadius);
+
+    *((uint32_t*) cursor) = outlineAlpha;
+
+    *outLen = chunkLen;
+    return buffer;
+}
+
+::std::ostream& operator<<(::std::ostream& out, const Range& range) {
+    return out << "[" << range.start << ", " << range.end << ")";
+}
+
+::std::ostream& operator<<(::std::ostream& out, const Bounds& bounds) {
+    return out << "l=" << bounds.left
+            << " t=" << bounds.top
+            << " r=" << bounds.right
+            << " b=" << bounds.bottom;
+}
+
+::std::ostream& operator<<(::std::ostream& out, const NinePatch& ninePatch) {
+    return out << "padding: " << ninePatch.padding
+            << ", bounds: " << ninePatch.layoutBounds
+            << ", outline: " << ninePatch.outline
+            << " rad=" << ninePatch.outlineRadius
+            << " alpha=" << ninePatch.outlineAlpha;
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/compile/NinePatch_test.cpp b/tools/aapt2/compile/NinePatch_test.cpp
new file mode 100644
index 0000000..ac4ee02
--- /dev/null
+++ b/tools/aapt2/compile/NinePatch_test.cpp
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2016 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 "compile/Image.h"
+#include "test/Test.h"
+
+namespace aapt {
+
+// Pixels are in RGBA_8888 packing.
+
+#define RED   "\xff\x00\x00\xff"
+#define BLUE  "\x00\x00\xff\xff"
+#define GREEN "\xff\x00\x00\xff"
+#define GR_70 "\xff\x00\x00\xb3"
+#define GR_50 "\xff\x00\x00\x80"
+#define GR_20 "\xff\x00\x00\x33"
+#define BLACK "\x00\x00\x00\xff"
+#define WHITE "\xff\xff\xff\xff"
+#define TRANS "\x00\x00\x00\x00"
+
+static uint8_t* k2x2[] = {
+        (uint8_t*) WHITE WHITE,
+        (uint8_t*) WHITE WHITE,
+};
+
+static uint8_t* kMixedNeutralColor3x3[] = {
+        (uint8_t*) WHITE BLACK TRANS,
+        (uint8_t*) TRANS RED   TRANS,
+        (uint8_t*) WHITE WHITE WHITE,
+};
+
+static uint8_t* kTransparentNeutralColor3x3[] = {
+        (uint8_t*) TRANS BLACK TRANS,
+        (uint8_t*) BLACK RED   BLACK,
+        (uint8_t*) TRANS BLACK TRANS,
+};
+
+static uint8_t* kSingleStretch7x6[] = {
+        (uint8_t*) WHITE WHITE BLACK BLACK BLACK WHITE WHITE,
+        (uint8_t*) WHITE RED   RED   RED   RED   RED   WHITE,
+        (uint8_t*) BLACK RED   RED   RED   RED   RED   WHITE,
+        (uint8_t*) BLACK RED   RED   RED   RED   RED   WHITE,
+        (uint8_t*) WHITE RED   RED   RED   RED   RED   WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE WHITE WHITE,
+};
+
+static uint8_t* kMultipleStretch10x7[] = {
+        (uint8_t*) WHITE WHITE BLACK WHITE BLACK BLACK WHITE BLACK WHITE WHITE,
+        (uint8_t*) BLACK RED   BLUE  RED   BLUE  BLUE  RED   BLUE  RED   WHITE,
+        (uint8_t*) BLACK RED   BLUE  RED   BLUE  BLUE  RED   BLUE  RED   WHITE,
+        (uint8_t*) WHITE RED   BLUE  RED   BLUE  BLUE  RED   BLUE  RED   WHITE,
+        (uint8_t*) BLACK RED   BLUE  RED   BLUE  BLUE  RED   BLUE  RED   WHITE,
+        (uint8_t*) BLACK RED   BLUE  RED   BLUE  BLUE  RED   BLUE  RED   WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE,
+};
+
+static uint8_t* kPadding6x5[] = {
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE BLACK,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE BLACK BLACK WHITE WHITE,
+};
+
+static uint8_t* kLayoutBoundsWrongEdge3x3[] = {
+        (uint8_t*) WHITE RED   WHITE,
+        (uint8_t*) RED   WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE,
+};
+
+static uint8_t* kLayoutBoundsNotEdgeAligned5x5[] = {
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE RED,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE RED   WHITE WHITE,
+};
+
+static uint8_t* kLayoutBounds5x5[] = {
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE RED,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE RED,
+        (uint8_t*) WHITE RED   WHITE RED   WHITE,
+};
+
+static uint8_t* kAsymmetricLayoutBounds5x5[] = {
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE RED,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE RED   WHITE WHITE WHITE,
+};
+
+static uint8_t* kPaddingAndLayoutBounds5x5[] = {
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE RED,
+        (uint8_t*) WHITE WHITE WHITE WHITE BLACK,
+        (uint8_t*) WHITE WHITE WHITE WHITE RED,
+        (uint8_t*) WHITE RED   BLACK RED   WHITE,
+};
+
+static uint8_t* kColorfulImage5x5[] = {
+        (uint8_t*) WHITE BLACK WHITE BLACK WHITE,
+        (uint8_t*) BLACK RED   BLUE  GREEN WHITE,
+        (uint8_t*) BLACK RED   GREEN GREEN WHITE,
+        (uint8_t*) WHITE TRANS BLUE  GREEN WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+};
+
+static uint8_t* kOutlineOpaque10x10[] = {
+        (uint8_t*) WHITE BLACK BLACK BLACK BLACK BLACK BLACK BLACK BLACK WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE,
+};
+
+static uint8_t* kOutlineTranslucent10x10[] = {
+        (uint8_t*) WHITE BLACK BLACK BLACK BLACK BLACK BLACK BLACK BLACK WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE,
+        (uint8_t*) WHITE TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE,
+};
+
+static uint8_t* kOutlineOffsetTranslucent12x10[] = {
+        (uint8_t*) WHITE WHITE WHITE BLACK BLACK BLACK BLACK BLACK BLACK BLACK BLACK WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE,
+        (uint8_t*) WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE,
+};
+
+static uint8_t* kOutlineRadius5x5[] = {
+        (uint8_t*) WHITE BLACK BLACK BLACK WHITE,
+        (uint8_t*) BLACK TRANS GREEN TRANS WHITE,
+        (uint8_t*) BLACK GREEN GREEN GREEN WHITE,
+        (uint8_t*) BLACK TRANS GREEN TRANS WHITE,
+        (uint8_t*) WHITE WHITE WHITE WHITE WHITE,
+};
+
+TEST(NinePatchTest, Minimum3x3) {
+    std::string err;
+    EXPECT_EQ(nullptr, NinePatch::create(k2x2, 2, 2, &err));
+    EXPECT_FALSE(err.empty());
+}
+
+TEST(NinePatchTest, MixedNeutralColors) {
+    std::string err;
+    EXPECT_EQ(nullptr, NinePatch::create(kMixedNeutralColor3x3, 3, 3, &err));
+    EXPECT_FALSE(err.empty());
+}
+
+TEST(NinePatchTest, TransparentNeutralColor) {
+    std::string err;
+    EXPECT_NE(nullptr, NinePatch::create(kTransparentNeutralColor3x3, 3, 3, &err));
+}
+
+TEST(NinePatchTest, SingleStretchRegion) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kSingleStretch7x6, 7, 6, &err);
+    ASSERT_NE(nullptr, ninePatch);
+
+    ASSERT_EQ(1u, ninePatch->horizontalStretchRegions.size());
+    ASSERT_EQ(1u, ninePatch->verticalStretchRegions.size());
+
+    EXPECT_EQ(Range(1, 4), ninePatch->horizontalStretchRegions.front());
+    EXPECT_EQ(Range(1, 3), ninePatch->verticalStretchRegions.front());
+}
+
+TEST(NinePatchTest, MultipleStretchRegions) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kMultipleStretch10x7, 10, 7, &err);
+    ASSERT_NE(nullptr, ninePatch);
+
+    ASSERT_EQ(3u, ninePatch->horizontalStretchRegions.size());
+    ASSERT_EQ(2u, ninePatch->verticalStretchRegions.size());
+
+    EXPECT_EQ(Range(1, 2), ninePatch->horizontalStretchRegions[0]);
+    EXPECT_EQ(Range(3, 5), ninePatch->horizontalStretchRegions[1]);
+    EXPECT_EQ(Range(6, 7), ninePatch->horizontalStretchRegions[2]);
+
+    EXPECT_EQ(Range(0, 2), ninePatch->verticalStretchRegions[0]);
+    EXPECT_EQ(Range(3, 5), ninePatch->verticalStretchRegions[1]);
+}
+
+TEST(NinePatchTest, InferPaddingFromStretchRegions) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kMultipleStretch10x7, 10, 7, &err);
+    ASSERT_NE(nullptr, ninePatch);
+    EXPECT_EQ(Bounds(1, 0, 1, 0), ninePatch->padding);
+}
+
+TEST(NinePatchTest, Padding) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kPadding6x5, 6, 5, &err);
+    ASSERT_NE(nullptr, ninePatch);
+    EXPECT_EQ(Bounds(1, 1, 1, 1), ninePatch->padding);
+}
+
+TEST(NinePatchTest, LayoutBoundsAreOnWrongEdge) {
+    std::string err;
+    EXPECT_EQ(nullptr, NinePatch::create(kLayoutBoundsWrongEdge3x3, 3, 3, &err));
+    EXPECT_FALSE(err.empty());
+}
+
+TEST(NinePatchTest, LayoutBoundsMustTouchEdges) {
+    std::string err;
+    EXPECT_EQ(nullptr, NinePatch::create(kLayoutBoundsNotEdgeAligned5x5, 5, 5, &err));
+    EXPECT_FALSE(err.empty());
+}
+
+TEST(NinePatchTest, LayoutBounds) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kLayoutBounds5x5, 5, 5, &err);
+    ASSERT_NE(nullptr, ninePatch);
+    EXPECT_EQ(Bounds(1, 1, 1, 1), ninePatch->layoutBounds);
+
+    ninePatch = NinePatch::create(kAsymmetricLayoutBounds5x5, 5, 5, &err);
+    ASSERT_NE(nullptr, ninePatch);
+    EXPECT_EQ(Bounds(1, 1, 0, 0), ninePatch->layoutBounds);
+}
+
+TEST(NinePatchTest, PaddingAndLayoutBounds) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kPaddingAndLayoutBounds5x5, 5, 5,
+                                                             &err);
+    ASSERT_NE(nullptr, ninePatch);
+    EXPECT_EQ(Bounds(1, 1, 1, 1), ninePatch->padding);
+    EXPECT_EQ(Bounds(1, 1, 1, 1), ninePatch->layoutBounds);
+}
+
+TEST(NinePatchTest, RegionColorsAreCorrect) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kColorfulImage5x5, 5, 5, &err);
+    ASSERT_NE(nullptr, ninePatch);
+
+    std::vector<uint32_t> expectedColors = {
+            NinePatch::packRGBA((uint8_t*) RED),
+            (uint32_t) android::Res_png_9patch::NO_COLOR,
+            NinePatch::packRGBA((uint8_t*) GREEN),
+            (uint32_t) android::Res_png_9patch::TRANSPARENT_COLOR,
+            NinePatch::packRGBA((uint8_t*) BLUE),
+            NinePatch::packRGBA((uint8_t*) GREEN),
+    };
+    EXPECT_EQ(expectedColors, ninePatch->regionColors);
+}
+
+TEST(NinePatchTest, OutlineFromOpaqueImage) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kOutlineOpaque10x10, 10, 10, &err);
+    ASSERT_NE(nullptr, ninePatch);
+    EXPECT_EQ(Bounds(2, 2, 2, 2), ninePatch->outline);
+    EXPECT_EQ(0x000000ffu, ninePatch->outlineAlpha);
+    EXPECT_EQ(0.0f, ninePatch->outlineRadius);
+}
+
+TEST(NinePatchTest, OutlineFromTranslucentImage) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kOutlineTranslucent10x10, 10, 10,
+                                                             &err);
+    ASSERT_NE(nullptr, ninePatch);
+    EXPECT_EQ(Bounds(3, 3, 3, 3), ninePatch->outline);
+    EXPECT_EQ(0x000000b3u, ninePatch->outlineAlpha);
+    EXPECT_EQ(0.0f, ninePatch->outlineRadius);
+}
+
+TEST(NinePatchTest, OutlineFromOffCenterImage) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kOutlineOffsetTranslucent12x10, 12, 10,
+                                                             &err);
+    ASSERT_NE(nullptr, ninePatch);
+
+    // TODO(adamlesinski): The old AAPT algorithm searches from the outside to the middle
+    // for each inset. If the outline is shifted, the search may not find a closer bounds.
+    // This check should be:
+    //   EXPECT_EQ(Bounds(5, 3, 3, 3), ninePatch->outline);
+    // but until I know what behaviour I'm breaking, I will leave it at the incorrect:
+    EXPECT_EQ(Bounds(4, 3, 3, 3), ninePatch->outline);
+
+    EXPECT_EQ(0x000000b3u, ninePatch->outlineAlpha);
+    EXPECT_EQ(0.0f, ninePatch->outlineRadius);
+}
+
+TEST(NinePatchTest, OutlineRadius) {
+    std::string err;
+    std::unique_ptr<NinePatch> ninePatch = NinePatch::create(kOutlineRadius5x5, 5, 5, &err);
+    ASSERT_NE(nullptr, ninePatch);
+    EXPECT_EQ(Bounds(0, 0, 0, 0), ninePatch->outline);
+    EXPECT_EQ(3.4142f, ninePatch->outlineRadius);
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/compile/Png.h b/tools/aapt2/compile/Png.h
index f835b06e..4a15d95 100644
--- a/tools/aapt2/compile/Png.h
+++ b/tools/aapt2/compile/Png.h
@@ -17,10 +17,14 @@
 #ifndef AAPT_PNG_H
 #define AAPT_PNG_H
 
-#include "util/BigBuffer.h"
 #include "Diagnostics.h"
 #include "Source.h"
+#include "compile/Image.h"
+#include "io/Io.h"
+#include "process/IResourceTableConsumer.h"
+#include "util/BigBuffer.h"
 
+#include <android-base/macros.h>
 #include <iostream>
 #include <string>
 
@@ -40,8 +44,51 @@
 
 private:
     IDiagnostics* mDiag;
+
+    DISALLOW_COPY_AND_ASSIGN(Png);
 };
 
+/**
+ * An InputStream that filters out unimportant PNG chunks.
+ */
+class PngChunkFilter : public io::InputStream {
+public:
+    explicit PngChunkFilter(const StringPiece& data);
+
+    bool Next(const void** buffer, int* len) override;
+    void BackUp(int count) override;
+    bool Skip(int count) override;
+
+    int64_t ByteCount() const override {
+        return static_cast<int64_t>(mWindowStart);
+    }
+
+    bool HadError() const override {
+        return mError;
+    }
+
+private:
+    bool consumeWindow(const void** buffer, int* len);
+
+    StringPiece mData;
+    size_t mWindowStart = 0;
+    size_t mWindowEnd = 0;
+    bool mError = false;
+
+    DISALLOW_COPY_AND_ASSIGN(PngChunkFilter);
+};
+
+/**
+ * Reads a PNG from the InputStream into memory as an RGBA Image.
+ */
+std::unique_ptr<Image> readPng(IAaptContext* context, io::InputStream* in);
+
+/**
+ * Writes the RGBA Image, with optional 9-patch meta-data, into the OutputStream as a PNG.
+ */
+bool writePng(IAaptContext* context, const Image* image, const NinePatch* ninePatch,
+              io::OutputStream* out, const PngOptions& options);
+
 } // namespace aapt
 
 #endif // AAPT_PNG_H
diff --git a/tools/aapt2/compile/PngChunkFilter.cpp b/tools/aapt2/compile/PngChunkFilter.cpp
new file mode 100644
index 0000000..70a881f
--- /dev/null
+++ b/tools/aapt2/compile/PngChunkFilter.cpp
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2016 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 "compile/Png.h"
+#include "io/Io.h"
+#include "util/StringPiece.h"
+
+namespace aapt {
+
+static constexpr const char* kPngSignature = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a";
+
+// Useful helper function that encodes individual bytes into a uint32
+// at compile time.
+constexpr uint32_t u32(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
+    return (((uint32_t) a) << 24)
+            | (((uint32_t) b) << 16)
+            | (((uint32_t) c) << 8)
+            | ((uint32_t) d);
+}
+
+// Whitelist of PNG chunk types that we want to keep in the resulting PNG.
+enum PngChunkTypes {
+    kPngChunkIHDR = u32(73, 72, 68, 82),
+    kPngChunkIDAT = u32(73, 68, 65, 84),
+    kPngChunkIEND = u32(73, 69, 78, 68),
+    kPngChunkPLTE = u32(80, 76, 84, 69),
+    kPngChunktRNS = u32(116, 82, 78, 83),
+    kPngChunksRGB = u32(115, 82, 71, 66),
+};
+
+static uint32_t peek32LE(const char* data) {
+    uint32_t word = ((uint32_t) data[0]) & 0x000000ff;
+    word <<= 8;
+    word |= ((uint32_t) data[1]) & 0x000000ff;
+    word <<= 8;
+    word |= ((uint32_t) data[2]) & 0x000000ff;
+    word <<= 8;
+    word |= ((uint32_t) data[3]) & 0x000000ff;
+    return word;
+}
+
+static bool isPngChunkWhitelisted(uint32_t type) {
+    switch (type) {
+    case kPngChunkIHDR:
+    case kPngChunkIDAT:
+    case kPngChunkIEND:
+    case kPngChunkPLTE:
+    case kPngChunktRNS:
+    case kPngChunksRGB:
+        return true;
+    default:
+        return false;
+    }
+}
+
+PngChunkFilter::PngChunkFilter(const StringPiece& data) : mData(data) {
+    if (util::stringStartsWith(mData, kPngSignature)) {
+        mWindowStart = 0;
+        mWindowEnd = strlen(kPngSignature);
+    } else {
+        mError = true;
+    }
+}
+
+bool PngChunkFilter::consumeWindow(const void** buffer, int* len) {
+    if (mWindowStart != mWindowEnd) {
+        // We have bytes to give from our window.
+        const int bytesRead = (int) (mWindowEnd - mWindowStart);
+        *buffer = mData.data() + mWindowStart;
+        *len = bytesRead;
+        mWindowStart = mWindowEnd;
+        return true;
+    }
+    return false;
+}
+
+bool PngChunkFilter::Next(const void** buffer, int* len) {
+    if (mError) {
+        return false;
+    }
+
+    // In case BackUp was called, we must consume the window.
+    if (consumeWindow(buffer, len)) {
+        return true;
+    }
+
+    // Advance the window as far as possible (until we meet a chunk that
+    // we want to strip).
+    while (mWindowEnd < mData.size()) {
+        // Chunk length (4 bytes) + type (4 bytes) + crc32 (4 bytes) = 12 bytes.
+        const size_t kMinChunkHeaderSize = 3 * sizeof(uint32_t);
+
+        // Is there enough room for a chunk header?
+        if (mData.size() - mWindowStart < kMinChunkHeaderSize) {
+            mError = true;
+            return false;
+        }
+
+        // Verify the chunk length.
+        const uint32_t chunkLen = peek32LE(mData.data() + mWindowEnd);
+        if (((uint64_t) chunkLen) + ((uint64_t) mWindowEnd) + sizeof(uint32_t) > mData.size()) {
+            // Overflow.
+            mError = true;
+            return false;
+        }
+
+        // Do we strip this chunk?
+        const uint32_t chunkType = peek32LE(mData.data() + mWindowEnd + sizeof(uint32_t));
+        if (isPngChunkWhitelisted(chunkType)) {
+            // Advance the window to include this chunk.
+            mWindowEnd += kMinChunkHeaderSize + chunkLen;
+        } else {
+            // We want to strip this chunk. If we accumulated a window,
+            // we must return the window now.
+            if (mWindowStart != mWindowEnd) {
+                break;
+            }
+
+            // The window is empty, so we can advance past this chunk
+            // and keep looking for the next good chunk,
+            mWindowEnd += kMinChunkHeaderSize + chunkLen;
+            mWindowStart = mWindowEnd;
+        }
+    }
+
+    if (consumeWindow(buffer, len)) {
+        return true;
+    }
+    return false;
+}
+
+void PngChunkFilter::BackUp(int count) {
+    if (mError) {
+        return;
+    }
+    mWindowStart -= count;
+}
+
+bool PngChunkFilter::Skip(int count) {
+    if (mError) {
+        return false;
+    }
+
+    const void* buffer;
+    int len;
+    while (count > 0) {
+        if (!Next(&buffer, &len)) {
+            return false;
+        }
+        if (len > count) {
+            BackUp(len - count);
+            count = 0;
+        } else {
+            count -= len;
+        }
+    }
+    return true;
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/compile/PngCrunch.cpp b/tools/aapt2/compile/PngCrunch.cpp
new file mode 100644
index 0000000..a2e3f4f
--- /dev/null
+++ b/tools/aapt2/compile/PngCrunch.cpp
@@ -0,0 +1,724 @@
+/*
+ * Copyright (C) 2016 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 "compile/Png.h"
+
+#include <algorithm>
+#include <android-base/errors.h>
+#include <android-base/macros.h>
+#include <png.h>
+#include <unordered_map>
+#include <unordered_set>
+#include <zlib.h>
+
+namespace aapt {
+
+// Size in bytes of the PNG signature.
+constexpr size_t kPngSignatureSize = 8u;
+
+/**
+ * Custom deleter that destroys libpng read and info structs.
+ */
+class PngReadStructDeleter {
+public:
+    explicit PngReadStructDeleter(png_structp readPtr, png_infop infoPtr) :
+            mReadPtr(readPtr), mInfoPtr(infoPtr) {
+    }
+
+    ~PngReadStructDeleter() {
+        png_destroy_read_struct(&mReadPtr, &mInfoPtr, nullptr);
+    }
+
+private:
+    png_structp mReadPtr;
+    png_infop mInfoPtr;
+
+    DISALLOW_COPY_AND_ASSIGN(PngReadStructDeleter);
+};
+
+/**
+ * Custom deleter that destroys libpng write and info structs.
+ */
+class PngWriteStructDeleter {
+public:
+    explicit PngWriteStructDeleter(png_structp writePtr, png_infop infoPtr) :
+            mWritePtr(writePtr), mInfoPtr(infoPtr) {
+    }
+
+    ~PngWriteStructDeleter() {
+        png_destroy_write_struct(&mWritePtr, &mInfoPtr);
+    }
+
+private:
+    png_structp mWritePtr;
+    png_infop mInfoPtr;
+
+    DISALLOW_COPY_AND_ASSIGN(PngWriteStructDeleter);
+};
+
+// Custom warning logging method that uses IDiagnostics.
+static void logWarning(png_structp pngPtr, png_const_charp warningMsg) {
+    IDiagnostics* diag = (IDiagnostics*) png_get_error_ptr(pngPtr);
+    diag->warn(DiagMessage() << warningMsg);
+}
+
+// Custom error logging method that uses IDiagnostics.
+static void logError(png_structp pngPtr, png_const_charp errorMsg) {
+    IDiagnostics* diag = (IDiagnostics*) png_get_error_ptr(pngPtr);
+    diag->error(DiagMessage() << errorMsg);
+}
+
+static void readDataFromStream(png_structp pngPtr, png_bytep buffer, png_size_t len) {
+    io::InputStream* in = (io::InputStream*) png_get_io_ptr(pngPtr);
+
+    const void* inBuffer;
+    int inLen;
+    if (!in->Next(&inBuffer, &inLen)) {
+        if (in->HadError()) {
+            std::string err = in->GetError();
+            png_error(pngPtr, err.c_str());
+        }
+        return;
+    }
+
+    const size_t bytesRead = std::min(static_cast<size_t>(inLen), len);
+    memcpy(buffer, inBuffer, bytesRead);
+    if (bytesRead != static_cast<size_t>(inLen)) {
+        in->BackUp(inLen - static_cast<int>(bytesRead));
+    }
+}
+
+static void writeDataToStream(png_structp pngPtr, png_bytep buffer, png_size_t len) {
+    io::OutputStream* out = (io::OutputStream*) png_get_io_ptr(pngPtr);
+
+    void* outBuffer;
+    int outLen;
+    while (len > 0) {
+        if (!out->Next(&outBuffer, &outLen)) {
+            if (out->HadError()) {
+                std::string err = out->GetError();
+                png_error(pngPtr, err.c_str());
+            }
+            return;
+        }
+
+        const size_t bytesWritten = std::min(static_cast<size_t>(outLen), len);
+        memcpy(outBuffer, buffer, bytesWritten);
+
+        // Advance the input buffer.
+        buffer += bytesWritten;
+        len -= bytesWritten;
+
+        // Advance the output buffer.
+        outLen -= static_cast<int>(bytesWritten);
+    }
+
+    // If the entire output buffer wasn't used, backup.
+    if (outLen > 0) {
+        out->BackUp(outLen);
+    }
+}
+
+std::unique_ptr<Image> readPng(IAaptContext* context, io::InputStream* in) {
+    // Read the first 8 bytes of the file looking for the PNG signature.
+    // Bail early if it does not match.
+    const png_byte* signature;
+    int bufferSize;
+    if (!in->Next((const void**) &signature, &bufferSize)) {
+        context->getDiagnostics()->error(DiagMessage()
+                                         << android::base::SystemErrorCodeToString(errno));
+        return {};
+    }
+
+    if (static_cast<size_t>(bufferSize) < kPngSignatureSize
+            || png_sig_cmp(signature, 0, kPngSignatureSize) != 0) {
+        context->getDiagnostics()->error(DiagMessage()
+                                         << "file signature does not match PNG signature");
+        return {};
+    }
+
+    // Start at the beginning of the first chunk.
+    in->BackUp(bufferSize - static_cast<int>(kPngSignatureSize));
+
+    // Create and initialize the png_struct with the default error and warning handlers.
+    // The header version is also passed in to ensure that this was built against the same
+    // version of libpng.
+    png_structp readPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+    if (readPtr == nullptr) {
+        context->getDiagnostics()->error(DiagMessage()
+                                         << "failed to create libpng read png_struct");
+        return {};
+    }
+
+    // Create and initialize the memory for image header and data.
+    png_infop infoPtr = png_create_info_struct(readPtr);
+    if (infoPtr == nullptr) {
+        context->getDiagnostics()->error(DiagMessage() << "failed to create libpng read png_info");
+        png_destroy_read_struct(&readPtr, nullptr, nullptr);
+        return {};
+    }
+
+    // Automatically release PNG resources at end of scope.
+    PngReadStructDeleter pngReadDeleter(readPtr, infoPtr);
+
+    // libpng uses longjmp to jump to an error handling routine.
+    // setjmp will only return true if it was jumped to, aka there was
+    // an error.
+    if (setjmp(png_jmpbuf(readPtr))) {
+        return {};
+    }
+
+    // Handle warnings ourselves via IDiagnostics.
+    png_set_error_fn(readPtr, (png_voidp) context->getDiagnostics(), logError, logWarning);
+
+    // Set up the read functions which read from our custom data sources.
+    png_set_read_fn(readPtr, (png_voidp) in, readDataFromStream);
+
+    // Skip the signature that we already read.
+    png_set_sig_bytes(readPtr, kPngSignatureSize);
+
+    // Read the chunk headers.
+    png_read_info(readPtr, infoPtr);
+
+    // Extract image meta-data from the various chunk headers.
+    uint32_t width, height;
+    int bitDepth, colorType, interlaceMethod, compressionMethod, filterMethod;
+    png_get_IHDR(readPtr, infoPtr, &width, &height, &bitDepth, &colorType, &interlaceMethod,
+                 &compressionMethod, &filterMethod);
+
+    // When the image is read, expand it so that it is in RGBA 8888 format
+    // so that image handling is uniform.
+
+    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);
+    }
+
+    if (interlaceMethod != PNG_INTERLACE_NONE) {
+        png_set_interlace_handling(readPtr);
+    }
+
+    // Once all the options for reading have been set, we need to flush
+    // them to libpng.
+    png_read_update_info(readPtr, infoPtr);
+
+    // 9-patch uses int32_t to index images, so we cap the image dimensions to something
+    // that can always be represented by 9-patch.
+    if (width > std::numeric_limits<int32_t>::max() ||
+            height > std::numeric_limits<int32_t>::max()) {
+        context->getDiagnostics()->error(DiagMessage() << "PNG image dimensions are too large: "
+                                         << width << "x" << height);
+        return {};
+    }
+
+    std::unique_ptr<Image> outputImage = util::make_unique<Image>();
+    outputImage->width = static_cast<int32_t>(width);
+    outputImage->height = static_cast<int32_t>(height);
+
+    const size_t rowBytes = png_get_rowbytes(readPtr, infoPtr);
+    assert(rowBytes == 4 * width); // RGBA
+
+    // Allocate one large block to hold the image.
+    outputImage->data = std::unique_ptr<uint8_t[]>(new uint8_t[height * rowBytes]);
+
+    // Create an array of rows that index into the data block.
+    outputImage->rows = std::unique_ptr<uint8_t*[]>(new uint8_t*[height]);
+    for (uint32_t h = 0; h < height; h++) {
+        outputImage->rows[h] = outputImage->data.get() + (h * rowBytes);
+    }
+
+    // Actually read the image pixels.
+    png_read_image(readPtr, outputImage->rows.get());
+
+    // Finish reading. This will read any other chunks after the image data.
+    png_read_end(readPtr, infoPtr);
+
+    return outputImage;
+}
+
+/**
+ * Experimentally chosen constant to be added to the overhead of using color type
+ * PNG_COLOR_TYPE_PALETTE to account for the uncompressability of the palette chunk.
+ * Without this, many small PNGs encoded with palettes are larger after compression than
+ * the same PNGs encoded as RGBA.
+ */
+constexpr static const size_t kPaletteOverheadConstant = 1024u * 10u;
+
+// Pick a color type by which to encode the image, based on which color type will take
+// the least amount of disk space.
+//
+// 9-patch images traditionally have not been encoded with palettes.
+// The original rationale was to avoid dithering until after scaling,
+// but I don't think this would be an issue with palettes. Either way,
+// our naive size estimation tends to be wrong for small images like 9-patches
+// and using palettes balloons the size of the resulting 9-patch.
+// In order to not regress in size, restrict 9-patch to not use palettes.
+
+// The options are:
+//
+// - RGB
+// - RGBA
+// - RGB + cheap alpha
+// - Color palette
+// - Color palette + cheap alpha
+// - Color palette + alpha palette
+// - Grayscale
+// - Grayscale + cheap alpha
+// - Grayscale + alpha
+//
+static int pickColorType(int32_t width, int32_t height,
+                         bool grayScale, bool convertibleToGrayScale, bool hasNinePatch,
+                         size_t colorPaletteSize, size_t alphaPaletteSize) {
+    const size_t paletteChunkSize = 16 + colorPaletteSize * 3;
+    const size_t alphaChunkSize = 16 + alphaPaletteSize;
+    const size_t colorAlphaDataChunkSize = 16 + 4 * width * height;
+    const size_t colorDataChunkSize = 16 + 3 * width * height;
+    const size_t grayScaleAlphaDataChunkSize = 16 + 2 * width * height;
+    const size_t paletteDataChunkSize = 16 + width * height;
+
+    if (grayScale) {
+        if (alphaPaletteSize == 0) {
+            // This is the smallest the data can be.
+            return PNG_COLOR_TYPE_GRAY;
+        } else if (colorPaletteSize <= 256 && !hasNinePatch) {
+            // This grayscale has alpha and can fit within a palette.
+            // See if it is worth fitting into a palette.
+            const size_t paletteThreshold = paletteChunkSize + alphaChunkSize +
+                    paletteDataChunkSize + kPaletteOverheadConstant;
+            if (grayScaleAlphaDataChunkSize > paletteThreshold) {
+                return PNG_COLOR_TYPE_PALETTE;
+            }
+        }
+        return PNG_COLOR_TYPE_GRAY_ALPHA;
+    }
+
+
+    if (colorPaletteSize <= 256 && !hasNinePatch) {
+        // This image can fit inside a palette. Let's see if it is worth it.
+        size_t totalSizeWithPalette = paletteDataChunkSize + paletteChunkSize;
+        size_t totalSizeWithoutPalette = colorDataChunkSize;
+        if (alphaPaletteSize > 0) {
+            totalSizeWithPalette += alphaPaletteSize;
+            totalSizeWithoutPalette = colorAlphaDataChunkSize;
+        }
+
+        if (totalSizeWithoutPalette > totalSizeWithPalette + kPaletteOverheadConstant) {
+            return PNG_COLOR_TYPE_PALETTE;
+        }
+    }
+
+    if (convertibleToGrayScale) {
+        if (alphaPaletteSize == 0) {
+            return PNG_COLOR_TYPE_GRAY;
+        } else {
+            return PNG_COLOR_TYPE_GRAY_ALPHA;
+        }
+    }
+
+    if (alphaPaletteSize == 0) {
+        return PNG_COLOR_TYPE_RGB;
+    }
+    return PNG_COLOR_TYPE_RGBA;
+}
+
+// Assigns indices to the color and alpha palettes, encodes them, and then invokes
+// png_set_PLTE/png_set_tRNS.
+// This must be done before writing image data.
+// Image data must be transformed to use the indices assigned within the palette.
+static void writePalette(png_structp writePtr, png_infop writeInfoPtr,
+                  std::unordered_map<uint32_t, int>* colorPalette,
+                  std::unordered_set<uint32_t>* alphaPalette) {
+    assert(colorPalette->size() <= 256);
+    assert(alphaPalette->size() <= 256);
+
+    // Populate the PNG palette struct and assign indices to the color
+    // palette.
+
+    // Colors in the alpha palette should have smaller indices.
+    // This will ensure that we can truncate the alpha palette if it is
+    // smaller than the color palette.
+    int index = 0;
+    for (uint32_t color : *alphaPalette) {
+        (*colorPalette)[color] = index++;
+    }
+
+    // Assign the rest of the entries.
+    for (auto& entry : *colorPalette) {
+        if (entry.second == -1) {
+            entry.second = index++;
+        }
+    }
+
+    // Create the PNG color palette struct.
+    auto colorPaletteBytes = std::unique_ptr<png_color[]>(new png_color[colorPalette->size()]);
+
+    std::unique_ptr<png_byte[]> alphaPaletteBytes;
+    if (!alphaPalette->empty()) {
+        alphaPaletteBytes = std::unique_ptr<png_byte[]>(new png_byte[alphaPalette->size()]);
+    }
+
+    for (const auto& entry : *colorPalette) {
+        const uint32_t color = entry.first;
+        const int index = entry.second;
+        assert(index >= 0);
+        assert(static_cast<size_t>(index) < colorPalette->size());
+
+        png_colorp slot = colorPaletteBytes.get() + index;
+        slot->red = color >> 24;
+        slot->green = color >> 16;
+        slot->blue = color >> 8;
+
+        const png_byte alpha = color & 0x000000ff;
+        if (alpha != 0xff && alphaPaletteBytes) {
+            assert(static_cast<size_t>(index) < alphaPalette->size());
+            alphaPaletteBytes[index] = alpha;
+        }
+    }
+
+    // The bytes get copied here, so it is safe to release colorPaletteBytes at the end of function
+    // scope.
+    png_set_PLTE(writePtr, writeInfoPtr, colorPaletteBytes.get(), colorPalette->size());
+
+    if (alphaPaletteBytes) {
+        png_set_tRNS(writePtr, writeInfoPtr, alphaPaletteBytes.get(), alphaPalette->size(),
+                     nullptr);
+    }
+}
+
+// Write the 9-patch custom PNG chunks to writeInfoPtr. This must be done before
+// writing image data.
+static void writeNinePatch(png_structp writePtr, png_infop writeInfoPtr,
+                           const NinePatch* ninePatch) {
+    // The order of the chunks is important.
+    // 9-patch code in older platforms expects the 9-patch chunk to
+    // be last.
+
+    png_unknown_chunk unknownChunks[3];
+    memset(unknownChunks, 0, sizeof(unknownChunks));
+
+    size_t index = 0;
+    size_t chunkLen = 0;
+
+    std::unique_ptr<uint8_t[]> serializedOutline =
+            ninePatch->serializeRoundedRectOutline(&chunkLen);
+    strcpy((char*) unknownChunks[index].name, "npOl");
+    unknownChunks[index].size = chunkLen;
+    unknownChunks[index].data = (png_bytep) serializedOutline.get();
+    unknownChunks[index].location = PNG_HAVE_PLTE;
+    index++;
+
+    std::unique_ptr<uint8_t[]> serializedLayoutBounds;
+    if (ninePatch->layoutBounds.nonZero()) {
+        serializedLayoutBounds = ninePatch->serializeLayoutBounds(&chunkLen);
+        strcpy((char*) unknownChunks[index].name, "npLb");
+        unknownChunks[index].size = chunkLen;
+        unknownChunks[index].data = (png_bytep) serializedLayoutBounds.get();
+        unknownChunks[index].location = PNG_HAVE_PLTE;
+        index++;
+    }
+
+    std::unique_ptr<uint8_t[]> serializedNinePatch = ninePatch->serializeBase(&chunkLen);
+    strcpy((char*) unknownChunks[index].name, "npTc");
+    unknownChunks[index].size = chunkLen;
+    unknownChunks[index].data = (png_bytep) serializedNinePatch.get();
+    unknownChunks[index].location = PNG_HAVE_PLTE;
+    index++;
+
+    // Handle all unknown chunks. We are manually setting the chunks here,
+    // so we will only ever handle our custom chunks.
+    png_set_keep_unknown_chunks(writePtr, PNG_HANDLE_CHUNK_ALWAYS, nullptr, 0);
+
+    // Set the actual chunks here. The data gets copied, so our buffers can
+    // safely go out of scope.
+    png_set_unknown_chunks(writePtr, writeInfoPtr, unknownChunks, index);
+}
+
+bool writePng(IAaptContext* context, const Image* image, const NinePatch* ninePatch,
+              io::OutputStream* out, const PngOptions& options) {
+    // Create and initialize the write png_struct with the default error and warning handlers.
+    // The header version is also passed in to ensure that this was built against the same
+    // version of libpng.
+    png_structp writePtr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
+                                                   nullptr, nullptr, nullptr);
+    if (writePtr == nullptr) {
+        context->getDiagnostics()->error(DiagMessage()
+                                         << "failed to create libpng write png_struct");
+        return false;
+    }
+
+    // Allocate memory to store image header data.
+    png_infop writeInfoPtr = png_create_info_struct(writePtr);
+    if (writeInfoPtr == nullptr) {
+        context->getDiagnostics()->error(DiagMessage() << "failed to create libpng write png_info");
+        png_destroy_write_struct(&writePtr, nullptr);
+        return false;
+    }
+
+    // Automatically release PNG resources at end of scope.
+    PngWriteStructDeleter pngWriteDeleter(writePtr, writeInfoPtr);
+
+    // libpng uses longjmp to jump to error handling routines.
+    // setjmp will return true only if it was jumped to, aka, there was an error.
+    if (setjmp(png_jmpbuf(writePtr))) {
+        return false;
+    }
+
+    // Handle warnings with our IDiagnostics.
+    png_set_error_fn(writePtr, (png_voidp) context->getDiagnostics(), logError, logWarning);
+
+    // Set up the write functions which write to our custom data sources.
+    png_set_write_fn(writePtr, (png_voidp) out, writeDataToStream, nullptr);
+
+    // We want small files and can take the performance hit to achieve this goal.
+    png_set_compression_level(writePtr, Z_BEST_COMPRESSION);
+
+    // Begin analysis of the image data.
+    // 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 (palette).
+    std::unordered_map<uint32_t, int> colorPalette;
+    std::unordered_set<uint32_t> alphaPalette;
+    bool needsToZeroRGBChannelsOfTransparentPixels = false;
+    bool grayScale = true;
+    int maxGrayDeviation = 0;
+
+    for (int32_t y = 0; y < image->height; y++) {
+        const uint8_t* row = image->rows[y];
+        for (int32_t x = 0; x < image->width; x++) {
+            int red = *row++;
+            int green = *row++;
+            int blue = *row++;
+            int alpha = *row++;
+
+            if (alpha == 0) {
+                // The color is completely transparent.
+                // For purposes of palettes and grayscale optimization,
+                // treat all channels as 0x00.
+                needsToZeroRGBChannelsOfTransparentPixels =
+                        needsToZeroRGBChannelsOfTransparentPixels ||
+                        (red != 0 || green != 0 || blue != 0);
+                red = green = blue = 0;
+            }
+
+            // Insert the color into the color palette.
+            const uint32_t color = red << 24 | green << 16 | blue << 8 | alpha;
+            colorPalette[color] = -1;
+
+            // If the pixel has non-opaque alpha, insert it into the
+            // alpha palette.
+            if (alpha != 0xff) {
+                alphaPalette.insert(color);
+            }
+
+            // Check if the image is indeed grayscale.
+            if (grayScale) {
+                if (red != green || red != blue) {
+                    grayScale = false;
+                }
+            }
+
+            // Calculate the gray scale deviation so that it can be compared
+            // with the threshold.
+            maxGrayDeviation = std::max(std::abs(red - green), maxGrayDeviation);
+            maxGrayDeviation = std::max(std::abs(green - blue), maxGrayDeviation);
+            maxGrayDeviation = std::max(std::abs(blue - red), maxGrayDeviation);
+        }
+    }
+
+    if (context->verbose()) {
+        DiagMessage msg;
+        msg << " paletteSize=" << colorPalette.size()
+                << " alphaPaletteSize=" << alphaPalette.size()
+                << " maxGrayDeviation=" << maxGrayDeviation
+                << " grayScale=" << (grayScale ? "true" : "false");
+        context->getDiagnostics()->note(msg);
+    }
+
+    const bool convertibleToGrayScale = maxGrayDeviation <= options.grayScaleTolerance;
+
+    const int newColorType = pickColorType(image->width, image->height, grayScale,
+                                           convertibleToGrayScale, ninePatch != nullptr,
+                                           colorPalette.size(), alphaPalette.size());
+
+    if (context->verbose()) {
+        DiagMessage msg;
+        msg << "encoding PNG ";
+        if (ninePatch) {
+            msg << "(with 9-patch) as ";
+        }
+        switch (newColorType) {
+        case PNG_COLOR_TYPE_GRAY:
+            msg << "GRAY";
+            break;
+        case PNG_COLOR_TYPE_GRAY_ALPHA:
+            msg << "GRAY + ALPHA";
+            break;
+        case PNG_COLOR_TYPE_RGB:
+            msg << "RGB";
+            break;
+        case PNG_COLOR_TYPE_RGB_ALPHA:
+            msg << "RGBA";
+            break;
+        case PNG_COLOR_TYPE_PALETTE:
+            msg << "PALETTE";
+            break;
+        default:
+            msg << "unknown type " << newColorType;
+            break;
+        }
+        context->getDiagnostics()->note(msg);
+    }
+
+    png_set_IHDR(writePtr, writeInfoPtr, image->width, image->height, 8, newColorType,
+                 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+
+    if (newColorType & PNG_COLOR_MASK_PALETTE) {
+        // Assigns indices to the palette, and writes the encoded palette to the libpng writePtr.
+        writePalette(writePtr, writeInfoPtr, &colorPalette, &alphaPalette);
+        png_set_filter(writePtr, 0, PNG_NO_FILTERS);
+    } else {
+        png_set_filter(writePtr, 0, PNG_ALL_FILTERS);
+    }
+
+    if (ninePatch) {
+        writeNinePatch(writePtr, writeInfoPtr, ninePatch);
+    }
+
+    // Flush our updates to the header.
+    png_write_info(writePtr, writeInfoPtr);
+
+    // Write out each row of image data according to its encoding.
+    if (newColorType == PNG_COLOR_TYPE_PALETTE) {
+        // 1 byte/pixel.
+        auto outRow = std::unique_ptr<png_byte[]>(new png_byte[image->width]);
+
+        for (int32_t y = 0; y < image->height; y++) {
+            png_const_bytep inRow = image->rows[y];
+            for (int32_t x = 0; x < image->width; x++) {
+                int rr = *inRow++;
+                int gg = *inRow++;
+                int bb = *inRow++;
+                int aa = *inRow++;
+                if (aa == 0) {
+                    // Zero out color channels when transparent.
+                    rr = gg = bb = 0;
+                }
+
+                const uint32_t color = rr << 24 | gg << 16 | bb << 8 | aa;
+                const int idx = colorPalette[color];
+                assert(idx != -1);
+                outRow[x] = static_cast<png_byte>(idx);
+            }
+            png_write_row(writePtr, outRow.get());
+        }
+    } else if (newColorType == PNG_COLOR_TYPE_GRAY || newColorType == PNG_COLOR_TYPE_GRAY_ALPHA) {
+        const size_t bpp = newColorType == PNG_COLOR_TYPE_GRAY ? 1 : 2;
+        auto outRow = std::unique_ptr<png_byte[]>(new png_byte[image->width * bpp]);
+
+        for (int32_t y = 0; y < image->height; y++) {
+            png_const_bytep inRow = image->rows[y];
+            for (int32_t x = 0; x < image->width; x++) {
+                int rr = inRow[x * 4];
+                int gg = inRow[x * 4 + 1];
+                int bb = inRow[x * 4 + 2];
+                int aa = inRow[x * 4 + 3];
+                if (aa == 0) {
+                    // Zero out the gray channel when transparent.
+                    rr = gg = bb = 0;
+                }
+
+                if (grayScale) {
+                    // The image was already grayscale, red == green == blue.
+                    outRow[x * bpp] = inRow[x * 4];
+                } else {
+                    // The image is convertible to grayscale, use linear-luminance of
+                    // sRGB colorspace: https://en.wikipedia.org/wiki/Grayscale#Colorimetric_.28luminance-preserving.29_conversion_to_grayscale
+                    outRow[x * bpp] = (png_byte) (rr * 0.2126f + gg * 0.7152f + bb * 0.0722f);
+                }
+
+                if (bpp == 2) {
+                    // Write out alpha if we have it.
+                    outRow[x * bpp + 1] = aa;
+                }
+            }
+            png_write_row(writePtr, outRow.get());
+        }
+    } else if (newColorType == PNG_COLOR_TYPE_RGB || newColorType == PNG_COLOR_TYPE_RGBA) {
+        const size_t bpp = newColorType == PNG_COLOR_TYPE_RGB ? 3 : 4;
+        if (needsToZeroRGBChannelsOfTransparentPixels) {
+            // The source RGBA data can't be used as-is, because we need to zero out the RGB
+            // values of transparent pixels.
+            auto outRow = std::unique_ptr<png_byte[]>(new png_byte[image->width * bpp]);
+
+            for (int32_t y = 0; y < image->height; y++) {
+                png_const_bytep inRow = image->rows[y];
+                for (int32_t x = 0; x < image->width; x++) {
+                    int rr = *inRow++;
+                    int gg = *inRow++;
+                    int bb = *inRow++;
+                    int aa = *inRow++;
+                    if (aa == 0) {
+                        // Zero out the RGB channels when transparent.
+                        rr = gg = bb = 0;
+                    }
+                    outRow[x * bpp] = rr;
+                    outRow[x * bpp + 1] = gg;
+                    outRow[x * bpp + 2] = bb;
+                    if (bpp == 4) {
+                        outRow[x * bpp + 3] = aa;
+                    }
+                }
+                png_write_row(writePtr, outRow.get());
+            }
+        } else {
+            // The source image can be used as-is, just tell libpng whether or not to ignore
+            // the alpha channel.
+            if (newColorType == PNG_COLOR_TYPE_RGB) {
+                // Delete the extraneous alpha values that we appended to our buffer
+                // when reading the original values.
+                png_set_filler(writePtr, 0, PNG_FILLER_AFTER);
+            }
+            png_write_image(writePtr, image->rows.get());
+        }
+    } else {
+        assert(false && "unreachable");
+    }
+
+    png_write_end(writePtr, writeInfoPtr);
+    return true;
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/cheap_transparency.png b/tools/aapt2/integration-tests/AppOne/res/drawable/cheap_transparency.png
new file mode 100644
index 0000000..0522a99
--- /dev/null
+++ b/tools/aapt2/integration-tests/AppOne/res/drawable/cheap_transparency.png
Binary files differ
diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/complex.9.png b/tools/aapt2/integration-tests/AppOne/res/drawable/complex.9.png
new file mode 100644
index 0000000..baf9fff
--- /dev/null
+++ b/tools/aapt2/integration-tests/AppOne/res/drawable/complex.9.png
Binary files differ
diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/outline_8x8.9.png b/tools/aapt2/integration-tests/AppOne/res/drawable/outline_8x8.9.png
new file mode 100644
index 0000000..7b331e1
--- /dev/null
+++ b/tools/aapt2/integration-tests/AppOne/res/drawable/outline_8x8.9.png
Binary files differ
diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/round_rect_off_center_outline_32x16.9.png b/tools/aapt2/integration-tests/AppOne/res/drawable/round_rect_off_center_outline_32x16.9.png
new file mode 100644
index 0000000..0ec6c70
--- /dev/null
+++ b/tools/aapt2/integration-tests/AppOne/res/drawable/round_rect_off_center_outline_32x16.9.png
Binary files differ
diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/round_rect_outline_32x16.9.png b/tools/aapt2/integration-tests/AppOne/res/drawable/round_rect_outline_32x16.9.png
new file mode 100644
index 0000000..e05708a
--- /dev/null
+++ b/tools/aapt2/integration-tests/AppOne/res/drawable/round_rect_outline_32x16.9.png
Binary files differ
diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/transparent_3x3.9.png b/tools/aapt2/integration-tests/AppOne/res/drawable/transparent_3x3.9.png
new file mode 100644
index 0000000..a11377a
--- /dev/null
+++ b/tools/aapt2/integration-tests/AppOne/res/drawable/transparent_3x3.9.png
Binary files differ
diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/transparent_optical_bounds_3x3.9.png b/tools/aapt2/integration-tests/AppOne/res/drawable/transparent_optical_bounds_3x3.9.png
new file mode 100644
index 0000000..6803e42
--- /dev/null
+++ b/tools/aapt2/integration-tests/AppOne/res/drawable/transparent_optical_bounds_3x3.9.png
Binary files differ
diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/white_3x3.9.png b/tools/aapt2/integration-tests/AppOne/res/drawable/white_3x3.9.png
new file mode 100644
index 0000000..1a3731b
--- /dev/null
+++ b/tools/aapt2/integration-tests/AppOne/res/drawable/white_3x3.9.png
Binary files differ
diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/white_optical_bounds_3x3.9.png b/tools/aapt2/integration-tests/AppOne/res/drawable/white_optical_bounds_3x3.9.png
new file mode 100644
index 0000000..489ace2
--- /dev/null
+++ b/tools/aapt2/integration-tests/AppOne/res/drawable/white_optical_bounds_3x3.9.png
Binary files differ
diff --git a/tools/aapt2/io/Io.cpp b/tools/aapt2/io/Io.cpp
new file mode 100644
index 0000000..963c21c
--- /dev/null
+++ b/tools/aapt2/io/Io.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 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 "io/Io.h"
+
+#include <algorithm>
+#include <cstring>
+
+namespace aapt {
+namespace io {
+
+bool copy(OutputStream* out, InputStream* in) {
+    const void* inBuffer;
+    int inLen;
+    while (in->Next(&inBuffer, &inLen)) {
+        void* outBuffer;
+        int outLen;
+        if (!out->Next(&outBuffer, &outLen)) {
+            return !out->HadError();
+        }
+
+        const int bytesToCopy = std::min(inLen, outLen);
+        memcpy(outBuffer, inBuffer, bytesToCopy);
+        out->BackUp(outLen - bytesToCopy);
+        in->BackUp(inLen - bytesToCopy);
+    }
+    return !in->HadError();
+}
+
+} // namespace io
+} // namespace aapt
diff --git a/tools/aapt2/io/Io.h b/tools/aapt2/io/Io.h
new file mode 100644
index 0000000..e1e9107
--- /dev/null
+++ b/tools/aapt2/io/Io.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#ifndef AAPT_IO_IO_H
+#define AAPT_IO_IO_H
+
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+#include <string>
+
+namespace aapt {
+namespace io {
+
+/**
+ * InputStream interface that inherits from protobuf's ZeroCopyInputStream,
+ * but adds error handling methods to better report issues.
+ *
+ * The code style here matches the protobuf style.
+ */
+class InputStream : public google::protobuf::io::ZeroCopyInputStream {
+public:
+    virtual std::string GetError() const {
+        return {};
+    }
+
+    virtual bool HadError() const = 0;
+};
+
+/**
+ * OutputStream interface that inherits from protobuf's ZeroCopyOutputStream,
+ * but adds error handling methods to better report issues.
+ *
+ * The code style here matches the protobuf style.
+ */
+class OutputStream : public google::protobuf::io::ZeroCopyOutputStream {
+public:
+    virtual std::string GetError() const {
+        return {};
+    }
+
+    virtual bool HadError() const = 0;
+};
+
+/**
+ * Copies the data from in to out. Returns true if there was no error.
+ * If there was an error, check the individual streams' HadError/GetError
+ * methods.
+ */
+bool copy(OutputStream* out, InputStream* in);
+
+} // namespace io
+} // namespace aapt
+
+#endif /* AAPT_IO_IO_H */
diff --git a/tools/aapt2/util/BigBuffer.cpp b/tools/aapt2/util/BigBuffer.cpp
index c88e3c1..de4ecd2 100644
--- a/tools/aapt2/util/BigBuffer.cpp
+++ b/tools/aapt2/util/BigBuffer.cpp
@@ -49,4 +49,29 @@
     return mBlocks.back().buffer.get();
 }
 
+void* BigBuffer::nextBlock(size_t* outSize) {
+    if (!mBlocks.empty()) {
+        Block& block = mBlocks.back();
+        if (block.size != block.mBlockSize) {
+            void* outBuffer = block.buffer.get() + block.size;
+            size_t size = block.mBlockSize - block.size;
+            block.size = block.mBlockSize;
+            mSize += size;
+            *outSize = size;
+            return outBuffer;
+        }
+    }
+
+    // Zero-allocate the block's buffer.
+    Block block = {};
+    block.buffer = std::unique_ptr<uint8_t[]>(new uint8_t[mBlockSize]());
+    assert(block.buffer);
+    block.size = mBlockSize;
+    block.mBlockSize = mBlockSize;
+    mBlocks.push_back(std::move(block));
+    mSize += mBlockSize;
+    *outSize = mBlockSize;
+    return mBlocks.back().buffer.get();
+}
+
 } // namespace aapt
diff --git a/tools/aapt2/util/BigBuffer.h b/tools/aapt2/util/BigBuffer.h
index ba8532f..685614f 100644
--- a/tools/aapt2/util/BigBuffer.h
+++ b/tools/aapt2/util/BigBuffer.h
@@ -82,6 +82,20 @@
     T* nextBlock(size_t count = 1);
 
     /**
+     * Returns the next block available and puts the size in outCount.
+     * This is useful for grabbing blocks where the size doesn't matter.
+     * Use backUp() to give back any bytes that were not used.
+     */
+    void* nextBlock(size_t* outCount);
+
+    /**
+     * Backs up count bytes. This must only be called after nextBlock()
+     * and can not be larger than sizeof(T) * count of the last nextBlock()
+     * call.
+     */
+    void backUp(size_t count);
+
+    /**
      * Moves the specified BigBuffer into this one. When this method
      * returns, buffer is empty.
      */
@@ -97,6 +111,8 @@
      */
     void align4();
 
+    size_t getBlockSize() const;
+
     const_iterator begin() const;
     const_iterator end() const;
 
@@ -123,6 +139,10 @@
     return mSize;
 }
 
+inline size_t BigBuffer::getBlockSize() const {
+    return mBlockSize;
+}
+
 template <typename T>
 inline T* BigBuffer::nextBlock(size_t count) {
     static_assert(std::is_standard_layout<T>::value, "T must be standard_layout type");
@@ -130,6 +150,12 @@
     return reinterpret_cast<T*>(nextBlockImpl(sizeof(T) * count));
 }
 
+inline void BigBuffer::backUp(size_t count) {
+    Block& block = mBlocks.back();
+    block.size -= count;
+    mSize -= count;
+}
+
 inline void BigBuffer::appendBuffer(BigBuffer&& buffer) {
     std::move(buffer.mBlocks.begin(), buffer.mBlocks.end(), std::back_inserter(mBlocks));
     mSize += buffer.mSize;
diff --git a/tools/aapt2/util/StringPiece.h b/tools/aapt2/util/StringPiece.h
index 4300a67..266c003 100644
--- a/tools/aapt2/util/StringPiece.h
+++ b/tools/aapt2/util/StringPiece.h
@@ -39,6 +39,9 @@
     using const_iterator = const TChar*;
     using difference_type = size_t;
 
+    // End of string marker.
+    constexpr static const size_t npos = static_cast<size_t>(-1);
+
     BasicStringPiece();
     BasicStringPiece(const BasicStringPiece<TChar>& str);
     BasicStringPiece(const std::basic_string<TChar>& str);  // NOLINT(implicit)
@@ -48,7 +51,7 @@
     BasicStringPiece<TChar>& operator=(const BasicStringPiece<TChar>& rhs);
     BasicStringPiece<TChar>& assign(const TChar* str, size_t len);
 
-    BasicStringPiece<TChar> substr(size_t start, size_t len) const;
+    BasicStringPiece<TChar> substr(size_t start, size_t len = npos) const;
     BasicStringPiece<TChar> substr(BasicStringPiece<TChar>::const_iterator begin,
                                    BasicStringPiece<TChar>::const_iterator end) const;
 
@@ -81,6 +84,9 @@
 //
 
 template <typename TChar>
+constexpr const size_t BasicStringPiece<TChar>::npos;
+
+template <typename TChar>
 inline BasicStringPiece<TChar>::BasicStringPiece() : mData(nullptr) , mLength(0) {
 }
 
@@ -127,7 +133,11 @@
 
 template <typename TChar>
 inline BasicStringPiece<TChar> BasicStringPiece<TChar>::substr(size_t start, size_t len) const {
-    if (start + len > mLength) {
+    if (len == npos) {
+        len = mLength - start;
+    }
+
+    if (start > mLength || start + len > mLength) {
         return BasicStringPiece<TChar>();
     }
     return BasicStringPiece<TChar>(mData + start, len);