| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/android/resources/etc1_utils.h" |
| |
| #include "base/files/file.h" |
| #include "base/memory/aligned_memory.h" |
| #include "base/numerics/byte_conversions.h" |
| #include "skia/ext/skia_utils_base.h" |
| #include "third_party/android_opengl/etc1/etc1.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "third_party/skia/include/core/SkData.h" |
| #include "third_party/skia/include/core/SkMallocPixelRef.h" |
| #include "third_party/skia/include/core/SkPixelRef.h" |
| #include "ui/android/buildflags.h" |
| #include "ui/android/ui_android_features.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/geometry/size.h" |
| |
| #if BUILDFLAG(UI_ANDROID_ENABLE_NEW_TEXTURE_COMPRESSOR) |
| #include "ui/android/texture_compressor/cxx.rs.h" |
| #endif |
| |
| namespace ui { |
| |
| namespace { |
| |
| const uint32_t kCompressedKey = 0xABABABAB; |
| const uint32_t kCurrentExtraVersion = 1; |
| |
| unsigned int NextPowerOfTwo(int a) { |
| DCHECK(a >= 0); |
| auto x = static_cast<unsigned int>(a); |
| --x; |
| x |= x >> 1u; |
| x |= x >> 2u; |
| x |= x >> 4u; |
| x |= x >> 8u; |
| x |= x >> 16u; |
| return x + 1; |
| } |
| |
| unsigned int RoundUpMod4(int a) { |
| DCHECK(a >= 0); |
| auto x = static_cast<unsigned int>(a); |
| return (x + 3u) & ~3u; |
| } |
| |
| // TODO(khushalsagar): This is a hack to ensure correct byte size computation |
| // for SkPixelRefs wrapping encoded data for ETC1 compressed bitmaps. We ideally |
| // shouldn't be using SkPixelRefs to wrap encoded data. |
| size_t ETC1RowBytes(int width) { |
| DCHECK_EQ(width & 1, 0); |
| return width / 2; |
| } |
| |
| bool WriteBigEndianU32ToFile(base::File* file, |
| base::StrictNumeric<uint32_t> v) { |
| return file->WriteAtCurrentPos(base::U32ToBigEndian(v)) == sizeof(v); |
| } |
| |
| bool WriteBigEndianFloatToFile(base::File* file, float v) { |
| return file->WriteAtCurrentPos(base::FloatToBigEndian(v)) == sizeof(v); |
| } |
| |
| bool ReadBigEndianU32FromFile(base::File* file, uint32_t* out) { |
| std::array<uint8_t, sizeof(*out)> buffer; |
| if (file->ReadAtCurrentPos(buffer).value_or(0u) != buffer.size()) { |
| return false; |
| } |
| *out = base::U32FromBigEndian(buffer); |
| return true; |
| } |
| bool ReadBigEndianFloatFromFile(base::File* file, float* out) { |
| std::array<uint8_t, sizeof(*out)> buffer; |
| if (file->ReadAtCurrentPos(buffer).value_or(0u) != buffer.size()) { |
| return false; |
| } |
| *out = base::FloatFromBigEndian(buffer); |
| return true; |
| } |
| |
| gfx::Size GetETCEncodedSize(const gfx::Size& bitmap_size, bool supports_npot) { |
| DCHECK(bitmap_size.width() >= 0); |
| DCHECK(bitmap_size.height() >= 0); |
| DCHECK(!bitmap_size.IsEmpty()); |
| |
| if (!supports_npot) { |
| return gfx::Size(NextPowerOfTwo(bitmap_size.width()), |
| NextPowerOfTwo(bitmap_size.height())); |
| } else { |
| return gfx::Size(RoundUpMod4(bitmap_size.width()), |
| RoundUpMod4(bitmap_size.height())); |
| } |
| } |
| |
| #if BUILDFLAG(UI_ANDROID_ENABLE_NEW_TEXTURE_COMPRESSOR) |
| // Check that `data` is sufficiently aligned for `T` and cast it to a Rust slice |
| // of `T`. |
| template <typename T> |
| rust::Slice<T> CastToAlignedSlice(void* data, size_t bytes) { |
| CHECK(base::IsAligned(data, alignof(T))); |
| return {reinterpret_cast<T*>(data), bytes / sizeof(T)}; |
| } |
| #endif |
| |
| } // namespace |
| |
| // static |
| sk_sp<SkPixelRef> Etc1::CompressBitmap(SkBitmap raw_data, |
| bool supports_etc_npot) { |
| if (raw_data.empty()) { |
| return nullptr; |
| } |
| |
| const gfx::Size raw_data_size(raw_data.width(), raw_data.height()); |
| const gfx::Size encoded_size = |
| GetETCEncodedSize(raw_data_size, supports_etc_npot); |
| constexpr size_t kPixelSize = 4; // For kARGB_8888_Config. |
| size_t stride = kPixelSize * raw_data_size.width(); |
| |
| size_t encoded_bytes = |
| etc1_get_encoded_data_size(encoded_size.width(), encoded_size.height()); |
| SkImageInfo info = |
| SkImageInfo::Make(encoded_size.width(), encoded_size.height(), |
| kUnknown_SkColorType, kUnpremul_SkAlphaType); |
| sk_sp<SkData> etc1_pixel_data(SkData::MakeUninitialized(encoded_bytes)); |
| sk_sp<SkPixelRef> etc1_pixel_ref(SkMallocPixelRef::MakeWithData( |
| info, ETC1RowBytes(encoded_size.width()), std::move(etc1_pixel_data))); |
| |
| #if BUILDFLAG(UI_ANDROID_ENABLE_NEW_TEXTURE_COMPRESSOR) |
| constexpr int kBlockSize = 4; |
| if (base::FeatureList::IsEnabled(kUseNewEtc1Encoder)) { |
| // We assume the input slice is aligned to 4 bytes, which seems to hold in |
| // practice. |
| compress_etc1(CastToAlignedSlice<const uint32_t>( |
| raw_data.getPixels(), raw_data.computeByteSize()), |
| CastToAlignedSlice<unsigned char>(etc1_pixel_ref->pixels(), |
| encoded_bytes), |
| raw_data.width(), raw_data.height(), |
| raw_data.rowBytesAsPixels(), |
| encoded_size.width() / kBlockSize); |
| return etc1_pixel_ref; |
| } |
| #endif |
| |
| if (etc1_encode_image( |
| reinterpret_cast<unsigned char*>(raw_data.getPixels()), |
| raw_data_size.width(), raw_data_size.height(), kPixelSize, stride, |
| reinterpret_cast<unsigned char*>(etc1_pixel_ref->pixels()), |
| encoded_size.width(), encoded_size.height())) { |
| etc1_pixel_ref->setImmutable(); |
| return etc1_pixel_ref; |
| } |
| |
| return nullptr; |
| } |
| |
| bool Etc1::WriteToFile(base::File* file, |
| const gfx::Size& content_size, |
| const float scale, |
| sk_sp<SkPixelRef> compressed_data) { |
| if (!file->IsValid()) { |
| return false; |
| } |
| |
| if (!WriteBigEndianU32ToFile(file, kCompressedKey)) { |
| return false; |
| } |
| |
| if (!WriteBigEndianU32ToFile( |
| file, base::checked_cast<uint32_t>(content_size.width()))) { |
| return false; |
| } |
| |
| if (!WriteBigEndianU32ToFile( |
| file, base::checked_cast<uint32_t>(content_size.height()))) { |
| return false; |
| } |
| |
| // Write ETC1 header. |
| CHECK(compressed_data->width() >= 0); |
| CHECK(compressed_data->height() >= 0); |
| unsigned width = static_cast<unsigned>(compressed_data->width()); |
| unsigned height = static_cast<unsigned>(compressed_data->height()); |
| |
| unsigned char etc1_buffer[ETC_PKM_HEADER_SIZE]; |
| etc1_pkm_format_header(etc1_buffer, width, height); |
| |
| // SAFETY: buffer interacts with external API. |
| int header_bytes_written = UNSAFE_BUFFERS(file->WriteAtCurrentPos( |
| reinterpret_cast<char*>(etc1_buffer), ETC_PKM_HEADER_SIZE)); |
| if (header_bytes_written != ETC_PKM_HEADER_SIZE) { |
| return false; |
| } |
| |
| int data_size = etc1_get_encoded_data_size(width, height); |
| // SAFETY: buffer interacts with external API. |
| int pixel_bytes_written = UNSAFE_BUFFERS(file->WriteAtCurrentPos( |
| reinterpret_cast<char*>(compressed_data->pixels()), data_size)); |
| if (pixel_bytes_written != data_size) { |
| return false; |
| } |
| |
| if (!WriteBigEndianU32ToFile(file, kCurrentExtraVersion)) { |
| return false; |
| } |
| |
| if (!WriteBigEndianFloatToFile(file, 1.f / scale)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool Etc1::ReadFromFile(base::File* file, |
| gfx::Size* out_content_size, |
| float* out_scale, |
| sk_sp<SkPixelRef>* out_pixels) { |
| if (!file->IsValid()) { |
| return false; |
| } |
| |
| uint32_t key = 0; |
| if (!ReadBigEndianU32FromFile(file, &key)) { |
| return false; |
| } |
| |
| if (key != kCompressedKey) { |
| return false; |
| } |
| |
| int content_width; |
| { |
| uint32_t val = 0; |
| if (!ReadBigEndianU32FromFile(file, &val) || val == 0u || |
| !base::IsValueInRangeForNumericType<int>(val)) { |
| return false; |
| } |
| content_width = base::checked_cast<int>(val); |
| } |
| |
| int content_height; |
| { |
| uint32_t val = 0; |
| if (!ReadBigEndianU32FromFile(file, &val) || val == 0u || |
| !base::IsValueInRangeForNumericType<int>(val)) { |
| return false; |
| } |
| content_height = base::checked_cast<int>(val); |
| } |
| |
| out_content_size->SetSize(content_width, content_height); |
| |
| // Read ETC1 header. |
| int header_bytes_read = 0; |
| unsigned char etc1_buffer[ETC_PKM_HEADER_SIZE]; |
| // SAFETY: buffer interacts with external API. |
| header_bytes_read = UNSAFE_BUFFERS(file->ReadAtCurrentPos( |
| reinterpret_cast<char*>(etc1_buffer), ETC_PKM_HEADER_SIZE)); |
| if (header_bytes_read != ETC_PKM_HEADER_SIZE) { |
| return false; |
| } |
| |
| if (!etc1_pkm_is_valid(etc1_buffer)) { |
| return false; |
| } |
| |
| int raw_width = 0; |
| raw_width = etc1_pkm_get_width(etc1_buffer); |
| if (raw_width <= 0) { |
| return false; |
| } |
| |
| int raw_height = 0; |
| raw_height = etc1_pkm_get_height(etc1_buffer); |
| if (raw_height <= 0) { |
| return false; |
| } |
| |
| // Do some simple sanity check validation. We can't have thumbnails larger |
| // than the max display size of the screen. We also can't have etc1 texture |
| // data larger than the next power of 2 up from that. |
| gfx::Size display_size = |
| display::Screen::Get()->GetPrimaryDisplay().GetSizeInPixel(); |
| int max_dimension = std::max(display_size.width(), display_size.height()); |
| |
| if (content_width > max_dimension || content_height > max_dimension || |
| static_cast<size_t>(raw_width) > NextPowerOfTwo(max_dimension) || |
| static_cast<size_t>(raw_height) > NextPowerOfTwo(max_dimension)) { |
| return false; |
| } |
| |
| int data_size = etc1_get_encoded_data_size(raw_width, raw_height); |
| sk_sp<SkData> etc1_pixel_data(SkData::MakeUninitialized(data_size)); |
| |
| std::optional<size_t> pixel_bytes_read = |
| file->ReadAtCurrentPos(skia::as_writable_byte_span(*etc1_pixel_data)); |
| |
| if (pixel_bytes_read != data_size) { |
| return false; |
| } |
| |
| SkImageInfo info = SkImageInfo::Make( |
| raw_width, raw_height, kUnknown_SkColorType, kUnpremul_SkAlphaType); |
| |
| *out_pixels = SkMallocPixelRef::MakeWithData(info, ETC1RowBytes(raw_width), |
| std::move(etc1_pixel_data)); |
| |
| uint32_t extra_data_version = 0; |
| if (!ReadBigEndianU32FromFile(file, &extra_data_version)) { |
| return false; |
| } |
| |
| *out_scale = 1.f; |
| if (extra_data_version == 1u) { |
| if (!ReadBigEndianFloatFromFile(file, out_scale)) { |
| return false; |
| } |
| |
| if (*out_scale == 0.f) { |
| return false; |
| } |
| |
| *out_scale = 1.f / *out_scale; |
| } |
| |
| return true; |
| } |
| |
| } // namespace ui |