diff options
author | Adam Lesinski <adamlesinski@google.com> | 2016-10-03 21:22:24 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2016-10-03 21:22:28 +0000 |
commit | ffa9656223c974191255dff3960f45e134f97c8c (patch) | |
tree | 024099ef7c7656b0318b7882ca1f08b74479eee2 | |
parent | 996b2083da1181fbfb61c6993329b84f9218099c (diff) | |
parent | 21efb6827cede06c2ab708de6cdb64d052dddcce (diff) |
Merge "AAPT2: Refactor PngCrunching"
22 files changed, 2439 insertions, 8 deletions
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk index 18a1fbf725b4..b57d4db1fdf9 100644 --- a/tools/aapt2/Android.mk +++ b/tools/aapt2/Android.mk @@ -24,7 +24,10 @@ main := Main.cpp 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 @@ sources := \ flatten/XmlFlattener.cpp \ io/File.cpp \ io/FileSystem.cpp \ + io/Io.cpp \ io/ZipArchive.cpp \ link/AutoVersioner.cpp \ link/ManifestFixer.cpp \ @@ -84,6 +88,7 @@ sourcesJni := 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 e0f37ec37b92..dbd8062e8b36 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 flattenXmlToOutStream(IAaptContext* context, const StringPiece& outp 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 @@ static bool compileXml(IAaptContext* context, const CompileOptions& options, 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 @@ static bool compilePng(IAaptContext* context, const CompileOptions& options, 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; + } + + 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; } - Png png(context->getDiagnostics()); - if (!png.process(pathData.source, &fin, &buffer, {})) { + 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 compilePng(IAaptContext* context, const CompileOptions& options, 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 000000000000..fda6a3a903b0 --- /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 000000000000..408ecf71a44f --- /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 000000000000..ac4ee0207809 --- /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 f835b06e762b..4a15d95f36cc 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 @@ public: 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 000000000000..70a881f45382 --- /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 000000000000..a2e3f4fc1825 --- /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 Binary files differnew file mode 100644 index 000000000000..0522a9979db9 --- /dev/null +++ b/tools/aapt2/integration-tests/AppOne/res/drawable/cheap_transparency.png diff --git a/tools/aapt2/integration-tests/AppOne/res/drawable/complex.9.png b/tools/aapt2/integration-tests/AppOne/res/drawable/complex.9.png Binary files differnew file mode 100644 index 000000000000..baf9fff13ab5 --- /dev/null +++ b/tools/aapt2/integration-tests/AppOne/res/drawable/complex.9.png 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 Binary files differnew file mode 100644 index 000000000000..7b331e16fcbd --- /dev/null +++ b/tools/aapt2/integration-tests/AppOne/res/drawable/outline_8x8.9.png 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 Binary files differnew file mode 100644 index 000000000000..0ec6c70a2b9f --- /dev/null +++ b/tools/aapt2/integration-tests/AppOne/res/drawable/round_rect_off_center_outline_32x16.9.png 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 Binary files differnew file mode 100644 index 000000000000..e05708a089a3 --- /dev/null +++ b/tools/aapt2/integration-tests/AppOne/res/drawable/round_rect_outline_32x16.9.png 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 Binary files differnew file mode 100644 index 000000000000..a11377a0d670 --- /dev/null +++ b/tools/aapt2/integration-tests/AppOne/res/drawable/transparent_3x3.9.png 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 Binary files differnew file mode 100644 index 000000000000..6803e4243484 --- /dev/null +++ b/tools/aapt2/integration-tests/AppOne/res/drawable/transparent_optical_bounds_3x3.9.png 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 Binary files differnew file mode 100644 index 000000000000..1a3731bbc8b8 --- /dev/null +++ b/tools/aapt2/integration-tests/AppOne/res/drawable/white_3x3.9.png 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 Binary files differnew file mode 100644 index 000000000000..489ace292e1f --- /dev/null +++ b/tools/aapt2/integration-tests/AppOne/res/drawable/white_optical_bounds_3x3.9.png diff --git a/tools/aapt2/io/Io.cpp b/tools/aapt2/io/Io.cpp new file mode 100644 index 000000000000..963c21c31806 --- /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 000000000000..e1e9107e388b --- /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 c88e3c102415..de4ecd21ec2d 100644 --- a/tools/aapt2/util/BigBuffer.cpp +++ b/tools/aapt2/util/BigBuffer.cpp @@ -49,4 +49,29 @@ void* BigBuffer::nextBlockImpl(size_t size) { 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 ba8532f829e6..685614f1b808 100644 --- a/tools/aapt2/util/BigBuffer.h +++ b/tools/aapt2/util/BigBuffer.h @@ -82,6 +82,20 @@ public: 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 @@ public: */ void align4(); + size_t getBlockSize() const; + const_iterator begin() const; const_iterator end() const; @@ -123,6 +139,10 @@ inline size_t BigBuffer::size() const { 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 @@ inline T* BigBuffer::nextBlock(size_t count) { 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 4300a67d3581..266c003ec264 100644 --- a/tools/aapt2/util/StringPiece.h +++ b/tools/aapt2/util/StringPiece.h @@ -39,6 +39,9 @@ public: 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 @@ public: 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 @@ using StringPiece16 = BasicStringPiece<char16_t>; // 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 @@ inline BasicStringPiece<TChar>& BasicStringPiece<TChar>::assign(const TChar* str 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); |