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
This commit is contained in:
Adam Lesinski
2016-09-14 17:35:43 -07:00
parent b0de46ef07
commit 21efb6827c
22 changed files with 2439 additions and 8 deletions

View File

@@ -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 \
@@ -80,6 +84,7 @@ sources += Format.proto
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 \

View File

@@ -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;
}
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 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);

201
tools/aapt2/compile/Image.h Normal file
View File

@@ -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 */

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

44
tools/aapt2/io/Io.cpp Normal file
View File

@@ -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

66
tools/aapt2/io/Io.h Normal file
View File

@@ -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 */

View File

@@ -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

View File

@@ -81,6 +81,20 @@ public:
template <typename T>
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;

View File

@@ -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;
@@ -80,6 +83,9 @@ using StringPiece16 = BasicStringPiece<char16_t>;
// BasicStringPiece implementation.
//
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);