From b4f83ff6a99aad503463c178f57d737f0ba05679 Mon Sep 17 00:00:00 2001 From: Jeremy Meyer Date: Thu, 30 Nov 2023 19:29:50 +0000 Subject: Revert^2 "Move some image/9patch code to androidfw" This reverts commit 917043bc2586743afda5a21386893fa8c787800b. Reason for revert: Roll forward with fix Test: Automatic Bug: 296324826 Change-Id: I42a0b48c02fd497b2174c0c65f300265202f7ab1 --- libs/androidfw/NinePatch.cpp | 682 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 682 insertions(+) create mode 100644 libs/androidfw/NinePatch.cpp (limited to 'libs/androidfw/NinePatch.cpp') diff --git a/libs/androidfw/NinePatch.cpp b/libs/androidfw/NinePatch.cpp new file mode 100644 index 000000000000..1fdbebfb6daa --- /dev/null +++ b/libs/androidfw/NinePatch.cpp @@ -0,0 +1,682 @@ +/* + * 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 +#include +#include + +#include "androidfw/Image.h" +#include "androidfw/ResourceTypes.h" +#include "androidfw/StringPiece.h" + +using android::StringPiece; + +namespace android { + +// 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 get_alpha(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 +static bool FillRanges(const ImageLine* image_line, const ColorValidator* color_validator, + std::vector* primary_ranges, std::vector* secondary_ranges, + std::string* out_err) { + const int32_t length = image_line->GetLength(); + + uint32_t last_color = 0xffffffffu; + for (int32_t idx = 1; idx < length - 1; idx++) { + const uint32_t color = image_line->GetColor(idx); + if (!color_validator->IsValidColor(color)) { + *out_err = "found an invalid color"; + return false; + } + + if (color != last_color) { + // We are ending a range. Which range? + // note: encode the x offset without the final 1 pixel border. + if (last_color == kPrimaryColor) { + primary_ranges->back().end = idx - 1; + } else if (last_color == kSecondaryColor) { + secondary_ranges->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) { + primary_ranges->push_back(Range(idx - 1, length - 2)); + } else if (color == kSecondaryColor) { + secondary_ranges->push_back(Range(idx - 1, length - 2)); + } + last_color = 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) + : rows_(rows), xoffset_(xoffset), yoffset_(yoffset), length_(length) { + } + + inline int32_t GetLength() const { + return length_; + } + + inline uint32_t GetColor(int32_t idx) const { + return NinePatch::PackRGBA(rows_[yoffset_] + (idx + xoffset_) * 4); + } + + private: + uint8_t** rows_; + int32_t xoffset_, yoffset_, length_; + + 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) + : rows_(rows), xoffset_(xoffset), yoffset_(yoffset), length_(length) { + } + + inline int32_t GetLength() const { + return length_; + } + + inline uint32_t GetColor(int32_t idx) const { + return NinePatch::PackRGBA(rows_[yoffset_ + idx] + (xoffset_ * 4)); + } + + private: + uint8_t** rows_; + int32_t xoffset_, yoffset_, length_; + + 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) + : rows_(rows), + xoffset_(xoffset), + yoffset_(yoffset), + xstep_(xstep), + ystep_(ystep), + length_(length) { + } + + inline int32_t GetLength() const { + return length_; + } + + inline uint32_t GetColor(int32_t idx) const { + return NinePatch::PackRGBA(rows_[yoffset_ + (idx * ystep_)] + ((idx + xoffset_) * xstep_) * 4); + } + + private: + uint8_t** rows_; + int32_t xoffset_, yoffset_, xstep_, ystep_, length_; + + DISALLOW_COPY_AND_ASSIGN(DiagonalImageLine); +}; + +class TransparentNeutralColorValidator : public ColorValidator { + public: + bool IsNeutralColor(uint32_t color) const override { + return get_alpha(color) == 0; + } +}; + +class WhiteNeutralColorValidator : public ColorValidator { + public: + bool IsNeutralColor(uint32_t color) const override { + return color == kColorOpaqueWhite; + } +}; + +inline static uint32_t get_alpha(uint32_t color) { + return (color & 0xff000000u) >> 24; +} + +static bool PopulateBounds(const std::vector& padding, + const std::vector& layout_bounds, + const std::vector& stretch_regions, const int32_t length, + int32_t* padding_start, int32_t* padding_end, int32_t* layout_start, + int32_t* layout_end, StringPiece edge_name, std::string* out_err) { + if (padding.size() > 1) { + std::stringstream err_stream; + err_stream << "too many padding sections on " << edge_name << " border"; + *out_err = err_stream.str(); + return false; + } + + *padding_start = 0; + *padding_end = 0; + if (!padding.empty()) { + const Range& range = padding.front(); + *padding_start = range.start; + *padding_end = length - range.end; + } else if (!stretch_regions.empty()) { + // No padding was defined. Compute the padding from the first and last + // stretch regions. + *padding_start = stretch_regions.front().start; + *padding_end = length - stretch_regions.back().end; + } + + if (layout_bounds.size() > 2) { + std::stringstream err_stream; + err_stream << "too many layout bounds sections on " << edge_name << " border"; + *out_err = err_stream.str(); + return false; + } + + *layout_start = 0; + *layout_end = 0; + if (layout_bounds.size() >= 1) { + const Range& range = layout_bounds.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 err_stream; + err_stream << "layout bounds on " << edge_name << " border must start at edge"; + *out_err = err_stream.str(); + return false; + } + *layout_start = range.end; + + if (layout_bounds.size() >= 2) { + const Range& range = layout_bounds.back(); + if (range.end != length) { + std::stringstream err_stream; + err_stream << "layout bounds on " << edge_name << " border must start at edge"; + *out_err = err_stream.str(); + return false; + } + *layout_end = length - range.start; + } + } + return true; +} + +static int32_t CalculateSegmentCount(const std::vector& stretch_regions, int32_t length) { + if (stretch_regions.size() == 0) { + return 0; + } + + const bool start_is_fixed = stretch_regions.front().start != 0; + const bool end_is_fixed = stretch_regions.back().end != length; + int32_t modifier = 0; + if (start_is_fixed && end_is_fixed) { + modifier = 1; + } else if (!start_is_fixed && !end_is_fixed) { + modifier = -1; + } + return static_cast(stretch_regions.size()) * 2 + modifier; +} + +static uint32_t GetRegionColor(uint8_t** rows, const Bounds& region) { + // Sample the first pixel to compare against. + const uint32_t expected_color = 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 (get_alpha(color) == 0) { + // The color is transparent. + // If the expectedColor is not transparent, NO_COLOR. + if (get_alpha(expected_color) != 0) { + return android::Res_png_9patch::NO_COLOR; + } + } else if (color != expected_color) { + return android::Res_png_9patch::NO_COLOR; + } + } + } + + if (get_alpha(expected_color) == 0) { + return android::Res_png_9patch::TRANSPARENT_COLOR; + } + return expected_color; +} + +// Fills out_colors with each 9-patch section's color. If the whole section is +// transparent, +// it gets the special TRANSPARENT color. If the whole section is the same +// color, it is assigned +// that color. Otherwise it gets the special NO_COLOR color. +// +// 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& horizontal_stretch_regions, + const std::vector& vertical_stretch_regions, + const int32_t width, const int32_t height, + std::vector* out_colors) { + int32_t next_top = 0; + Bounds bounds; + auto row_iter = vertical_stretch_regions.begin(); + while (next_top != height) { + if (row_iter != vertical_stretch_regions.end()) { + if (next_top != row_iter->start) { + // This is a fixed segment. + // Offset the bounds by 1 to accommodate the border. + bounds.top = next_top + 1; + bounds.bottom = row_iter->start + 1; + next_top = row_iter->start; + } else { + // This is a stretchy segment. + // Offset the bounds by 1 to accommodate the border. + bounds.top = row_iter->start + 1; + bounds.bottom = row_iter->end + 1; + next_top = row_iter->end; + ++row_iter; + } + } else { + // This is the end, fixed section. + // Offset the bounds by 1 to accommodate the border. + bounds.top = next_top + 1; + bounds.bottom = height + 1; + next_top = height; + } + + int32_t next_left = 0; + auto col_iter = horizontal_stretch_regions.begin(); + while (next_left != width) { + if (col_iter != horizontal_stretch_regions.end()) { + if (next_left != col_iter->start) { + // This is a fixed segment. + // Offset the bounds by 1 to accommodate the border. + bounds.left = next_left + 1; + bounds.right = col_iter->start + 1; + next_left = col_iter->start; + } else { + // This is a stretchy segment. + // Offset the bounds by 1 to accommodate the border. + bounds.left = col_iter->start + 1; + bounds.right = col_iter->end + 1; + next_left = col_iter->end; + ++col_iter; + } + } else { + // This is the end, fixed section. + // Offset the bounds by 1 to accommodate the border. + bounds.left = next_left + 1; + bounds.right = width + 1; + next_left = width; + } + out_colors->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 +static void FindOutlineInsets(const ImageLine* image_line, int32_t* out_start, int32_t* out_end) { + *out_start = 0; + *out_end = 0; + + const int32_t length = image_line->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 max_alpha = 0; + for (int32_t i = 0; i < mid1 && max_alpha != 0xff; i++) { + uint32_t alpha = get_alpha(image_line->GetColor(i)); + if (alpha > max_alpha) { + max_alpha = alpha; + *out_start = i; + } + } + + max_alpha = 0; + for (int32_t i = length - 1; i >= mid2 && max_alpha != 0xff; i--) { + uint32_t alpha = get_alpha(image_line->GetColor(i)); + if (alpha > max_alpha) { + max_alpha = alpha; + *out_end = length - (i + 1); + } + } + return; +} + +template +static uint32_t FindMaxAlpha(const ImageLine* image_line) { + const int32_t length = image_line->GetLength(); + uint32_t max_alpha = 0; + for (int32_t idx = 0; idx < length && max_alpha != 0xff; idx++) { + uint32_t alpha = get_alpha(image_line->GetColor(idx)); + if (alpha > max_alpha) { + max_alpha = alpha; + } + } + return max_alpha; +} + +// 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::Create(uint8_t** rows, const int32_t width, + const int32_t height, std::string* out_err) { + if (width < 3 || height < 3) { + *out_err = "image must be at least 3x3 (1x1 image with 1 pixel border)"; + return {}; + } + + std::vector horizontal_padding; + std::vector horizontal_layout_bounds; + std::vector vertical_padding; + std::vector vertical_layout_bounds; + std::vector unexpected_ranges; + std::unique_ptr color_validator; + + if (rows[0][3] == 0) { + color_validator = std::make_unique(); + } else if (PackRGBA(rows[0]) == kColorOpaqueWhite) { + color_validator = std::make_unique(); + } else { + *out_err = "top-left corner pixel must be either opaque white or transparent"; + return {}; + } + + // Private constructor, can't use make_unique. + auto nine_patch = std::unique_ptr(new NinePatch()); + + HorizontalImageLine top_row(rows, 0, 0, width); + if (!FillRanges(&top_row, color_validator.get(), &nine_patch->horizontal_stretch_regions, + &unexpected_ranges, out_err)) { + return {}; + } + + if (!unexpected_ranges.empty()) { + const Range& range = unexpected_ranges[0]; + std::stringstream err_stream; + err_stream << "found unexpected optical bounds (red pixel) on top border " + << "at x=" << range.start + 1; + *out_err = err_stream.str(); + return {}; + } + + VerticalImageLine left_col(rows, 0, 0, height); + if (!FillRanges(&left_col, color_validator.get(), &nine_patch->vertical_stretch_regions, + &unexpected_ranges, out_err)) { + return {}; + } + + if (!unexpected_ranges.empty()) { + const Range& range = unexpected_ranges[0]; + std::stringstream err_stream; + err_stream << "found unexpected optical bounds (red pixel) on left border " + << "at y=" << range.start + 1; + return {}; + } + + HorizontalImageLine bottom_row(rows, 0, height - 1, width); + if (!FillRanges(&bottom_row, color_validator.get(), &horizontal_padding, + &horizontal_layout_bounds, out_err)) { + return {}; + } + + if (!PopulateBounds(horizontal_padding, horizontal_layout_bounds, + nine_patch->horizontal_stretch_regions, width - 2, &nine_patch->padding.left, + &nine_patch->padding.right, &nine_patch->layout_bounds.left, + &nine_patch->layout_bounds.right, "bottom", out_err)) { + return {}; + } + + VerticalImageLine right_col(rows, width - 1, 0, height); + if (!FillRanges(&right_col, color_validator.get(), &vertical_padding, &vertical_layout_bounds, + out_err)) { + return {}; + } + + if (!PopulateBounds(vertical_padding, vertical_layout_bounds, + nine_patch->vertical_stretch_regions, height - 2, &nine_patch->padding.top, + &nine_patch->padding.bottom, &nine_patch->layout_bounds.top, + &nine_patch->layout_bounds.bottom, "right", out_err)) { + return {}; + } + + // Fill the region colors of the 9-patch. + const int32_t num_rows = CalculateSegmentCount(nine_patch->horizontal_stretch_regions, width - 2); + const int32_t num_cols = CalculateSegmentCount(nine_patch->vertical_stretch_regions, height - 2); + if ((int64_t)num_rows * (int64_t)num_cols > 0x7f) { + *out_err = "too many regions in 9-patch"; + return {}; + } + + nine_patch->region_colors.reserve(num_rows * num_cols); + CalculateRegionColors(rows, nine_patch->horizontal_stretch_regions, + nine_patch->vertical_stretch_regions, width - 2, height - 2, + &nine_patch->region_colors); + + // Compute the outline based on opacity. + + // Find left and right extent of 9-patch content on center row. + HorizontalImageLine mid_row(rows, 1, height / 2, width - 2); + FindOutlineInsets(&mid_row, &nine_patch->outline.left, &nine_patch->outline.right); + + // Find top and bottom extent of 9-patch content on center column. + VerticalImageLine mid_col(rows, width / 2, 1, height - 2); + FindOutlineInsets(&mid_col, &nine_patch->outline.top, &nine_patch->outline.bottom); + + const int32_t outline_width = (width - 2) - nine_patch->outline.left - nine_patch->outline.right; + const int32_t outline_height = + (height - 2) - nine_patch->outline.top - nine_patch->outline.bottom; + + // Find the largest alpha value within the outline area. + HorizontalImageLine outline_mid_row(rows, 1 + nine_patch->outline.left, + 1 + nine_patch->outline.top + (outline_height / 2), + outline_width); + VerticalImageLine outline_mid_col(rows, 1 + nine_patch->outline.left + (outline_width / 2), + 1 + nine_patch->outline.top, outline_height); + nine_patch->outline_alpha = + std::max(FindMaxAlpha(&outline_mid_row), FindMaxAlpha(&outline_mid_col)); + + // 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 + nine_patch->outline.left, 1 + nine_patch->outline.top, 1, 1, + std::min(outline_width, outline_height)); + int32_t top_left, bottom_right; + FindOutlineInsets(&diagonal, &top_left, &bottom_right); + + /* 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 + */ + nine_patch->outline_radius = 3.4142f * top_left; + return nine_patch; +} + +std::unique_ptr NinePatch::SerializeBase(size_t* outLen) const { + android::Res_png_9patch data; + data.numXDivs = static_cast(horizontal_stretch_regions.size()) * 2; + data.numYDivs = static_cast(vertical_stretch_regions.size()) * 2; + data.numColors = static_cast(region_colors.size()); + data.paddingLeft = padding.left; + data.paddingRight = padding.right; + data.paddingTop = padding.top; + data.paddingBottom = padding.bottom; + + auto buffer = std::unique_ptr(new uint8_t[data.serializedSize()]); + android::Res_png_9patch::serialize(data, (const int32_t*)horizontal_stretch_regions.data(), + (const int32_t*)vertical_stretch_regions.data(), + region_colors.data(), buffer.get()); + // Convert to file endianness. + reinterpret_cast(buffer.get())->deviceToFile(); + + *outLen = data.serializedSize(); + return buffer; +} + +std::unique_ptr NinePatch::SerializeLayoutBounds(size_t* out_len) const { + size_t chunk_len = sizeof(uint32_t) * 4; + auto buffer = std::unique_ptr(new uint8_t[chunk_len]); + uint8_t* cursor = buffer.get(); + + memcpy(cursor, &layout_bounds.left, sizeof(layout_bounds.left)); + cursor += sizeof(layout_bounds.left); + + memcpy(cursor, &layout_bounds.top, sizeof(layout_bounds.top)); + cursor += sizeof(layout_bounds.top); + + memcpy(cursor, &layout_bounds.right, sizeof(layout_bounds.right)); + cursor += sizeof(layout_bounds.right); + + memcpy(cursor, &layout_bounds.bottom, sizeof(layout_bounds.bottom)); + cursor += sizeof(layout_bounds.bottom); + + *out_len = chunk_len; + return buffer; +} + +std::unique_ptr NinePatch::SerializeRoundedRectOutline(size_t* out_len) const { + size_t chunk_len = sizeof(uint32_t) * 6; + auto buffer = std::unique_ptr(new uint8_t[chunk_len]); + 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) = outline_radius; + cursor += sizeof(outline_radius); + + *((uint32_t*)cursor) = outline_alpha; + + *out_len = chunk_len; + 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; +} + +template +std::ostream& operator<<(std::ostream& os, const std::vector& v) { + for (int i = 0; i < v.size(); ++i) { + os << v[i]; + if (i != v.size() - 1) os << " "; + } + return os; +} + +::std::ostream& operator<<(::std::ostream& out, const NinePatch& nine_patch) { + return out << "horizontalStretch:" << nine_patch.horizontal_stretch_regions + << " verticalStretch:" << nine_patch.vertical_stretch_regions + << " padding: " << nine_patch.padding << ", bounds: " << nine_patch.layout_bounds + << ", outline: " << nine_patch.outline << " rad=" << nine_patch.outline_radius + << " alpha=" << nine_patch.outline_alpha; +} + +} // namespace android -- cgit v1.2.3-59-g8ed1b