| // Copyright 2022 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/web_applications/os_integration/icns_encoder.h" |
| |
| #include <algorithm> |
| |
| #include "base/big_endian.h" |
| #include "base/files/file.h" |
| #include "base/notreached.h" |
| #include "base/numerics/checked_math.h" |
| #include "base/ranges/algorithm.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/gfx/codec/png_codec.h" |
| #include "ui/gfx/image/image.h" |
| |
| namespace web_app { |
| |
| namespace { |
| |
| // Mapping of image size to the type identifiers used in the .icns file format |
| // for the png representation of the image as well as the RGB and Alpha channel |
| // representations. |
| struct IcnsBlockTypes { |
| int size; |
| uint32_t png_type; |
| uint32_t image_type = 0; |
| uint32_t mask_type = 0; |
| }; |
| |
| constexpr IcnsBlockTypes kIcnsBlockTypes[] = { |
| {16, 'icp4', 'is32', 's8mk'}, |
| {32, 'icp5', 'il32', 'l8mk'}, |
| {48, 'icp6', 'ih32', 'h8mk'}, |
| {128, 'ic07'}, |
| {256, 'ic08'}, |
| {512, 'ic09'}, |
| }; |
| |
| std::vector<uint8_t> CreateBlockHeader(uint32_t type, size_t data_length) { |
| std::vector<uint8_t> result(8); |
| base::WriteBigEndian(reinterpret_cast<char*>(result.data()), type); |
| base::WriteBigEndian(reinterpret_cast<char*>(result.data() + 4), |
| base::checked_cast<uint32_t>(data_length + 8)); |
| return result; |
| } |
| |
| // Struct containing the red, green, blue and alpha channels extracted from an |
| // image as four separate vectors. |
| struct ImageBytes { |
| std::vector<uint8_t> r, g, b, a; |
| }; |
| |
| // Extracts the red, green, blue and alpha channels from `bitmap` as four |
| // separate vectors. The red, green and blue channels will contain the |
| // unpremultiplied values, as that is how data is stored in an .icns file. |
| ImageBytes ExtractImageBytes(const SkBitmap& bitmap) { |
| ImageBytes result; |
| const size_t pixel_count = bitmap.height() * bitmap.width(); |
| result.r.reserve(pixel_count); |
| result.g.reserve(pixel_count); |
| result.b.reserve(pixel_count); |
| result.a.reserve(pixel_count); |
| for (int y = 0; y < bitmap.height(); ++y) { |
| for (int x = 0; x < bitmap.width(); ++x) { |
| SkColor c = bitmap.getColor(x, y); |
| result.r.push_back(SkColorGetR(c)); |
| result.g.push_back(SkColorGetG(c)); |
| result.b.push_back(SkColorGetB(c)); |
| result.a.push_back(SkColorGetA(c)); |
| } |
| } |
| return result; |
| } |
| |
| } // namespace |
| |
| IcnsEncoder::Block::Block(uint32_t type, std::vector<uint8_t> data) |
| : type(type), data(std::move(data)) {} |
| IcnsEncoder::Block::~Block() = default; |
| IcnsEncoder::Block::Block(Block&&) = default; |
| IcnsEncoder::Block& IcnsEncoder::Block::operator=(Block&&) = default; |
| |
| IcnsEncoder::IcnsEncoder() = default; |
| IcnsEncoder::~IcnsEncoder() = default; |
| |
| bool IcnsEncoder::AddImage(const gfx::Image& image) { |
| if (image.IsEmpty()) |
| return false; |
| |
| SkBitmap bitmap = image.AsBitmap(); |
| if (bitmap.colorType() != kN32_SkColorType || |
| bitmap.width() != bitmap.height()) |
| return false; |
| |
| const IcnsBlockTypes* block_types = base::ranges::find_if( |
| kIcnsBlockTypes, [&](const auto& t) { return t.size == bitmap.width(); }); |
| if (block_types == std::end(kIcnsBlockTypes)) |
| return false; |
| |
| if (block_types->image_type != 0) { |
| // If there is a legacy image type for this size we should use that rather |
| // than the png format, as many places in Mac OS do not properly support png |
| // icons for sizes that also support a legacy format. |
| DCHECK(block_types->mask_type != 0); |
| ImageBytes bytes = ExtractImageBytes(bitmap); |
| std::vector<uint8_t> image_data; |
| AppendRLEImageData(bytes.r, &image_data); |
| AppendRLEImageData(bytes.g, &image_data); |
| AppendRLEImageData(bytes.b, &image_data); |
| AppendBlock(block_types->image_type, std::move(image_data)); |
| AppendBlock(block_types->mask_type, std::move(bytes.a)); |
| } else { |
| DCHECK(block_types->png_type != 0); |
| std::vector<uint8_t> png_data; |
| if (!gfx::PNGCodec::EncodeBGRASkBitmap( |
| bitmap, /*discard_transparancy=*/false, &png_data)) { |
| return false; |
| } |
| AppendBlock(block_types->png_type, std::move(png_data)); |
| } |
| return true; |
| } |
| |
| bool IcnsEncoder::WriteToFile(const base::FilePath& path) const { |
| // Build the Table of Contents, which is simply the headers of all the blocks |
| // concatenated. |
| Block toc('TOC '); |
| toc.data.reserve(8 * blocks_.size()); |
| for (const auto& block : blocks_) { |
| auto header = CreateBlockHeader(block.type, block.data.size()); |
| toc.data.insert(toc.data.end(), header.begin(), header.end()); |
| } |
| |
| size_t total_data_size = |
| total_block_size_ + toc.data.size() + kBlockHeaderSize; |
| |
| base::File output(path, base::File::Flags::FLAG_CREATE_ALWAYS | |
| base::File::Flags::FLAG_WRITE); |
| if (!output.IsValid()) |
| return false; |
| |
| if (!output.WriteAtCurrentPosAndCheck( |
| ::web_app::CreateBlockHeader('icns', total_data_size))) { |
| return false; |
| } |
| if (!WriteBlockToFile(output, toc)) |
| return false; |
| for (const auto& block : blocks_) { |
| if (!WriteBlockToFile(output, block)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // static |
| void IcnsEncoder::AppendRLEImageData(base::span<const uint8_t> data, |
| std::vector<uint8_t>* rle_data) { |
| // The packing loop is done with two pieces of state: |
| // - data: at any point in the loop this only contains the bytes that have |
| // not yet been written to the block |
| // - search_offset: this is the offset within |data| used to search for |
| // byte runs |
| // |
| // The code scours through the data, looking for runs of length greater than 3 |
| // (since only runs of 3 or longer can be compressed). As soon as a run is |
| // found, all the data up to `search_offset` is dumped as literal data, |
| // `data` is updated to only point at the remaining data, then the run is |
| // dumped (and `data` updated again), and then the search continues. |
| |
| size_t search_offset = 0; |
| |
| // Search for runs through the block of data, byte by byte. |
| while (search_offset < data.size()) { |
| uint8_t current_byte = data[search_offset]; |
| size_t run_length = 1; |
| while (search_offset + run_length < data.size() && run_length < 130 && |
| data[search_offset + run_length] == current_byte) { |
| ++run_length; |
| } |
| if (run_length >= 3) { |
| // A long-enough run was found. First, dump all the data before the run |
| // into the output block. |
| while (search_offset > 0) { |
| // Because uncompressed data runs max out at 128 bytes of data, cap the |
| // uncompressed run at 128 bytes. |
| base::span<const uint8_t> uncompressed_chunk = |
| data.first(std::min<size_t>(search_offset, 128)); |
| // Key byte values of 0..127 mean 1..128 bytes of uncompressed data. |
| uint8_t key_byte = uncompressed_chunk.size() - 1; |
| rle_data->push_back(key_byte); |
| rle_data->insert(rle_data->end(), uncompressed_chunk.begin(), |
| uncompressed_chunk.end()); |
| data = data.subspan(uncompressed_chunk.size()); |
| search_offset -= uncompressed_chunk.size(); |
| } |
| // Now that the output block is caught up, put the run that was just found |
| // into it. Key byte values of 128..255 mean 3..130 copies of the |
| // following byte, thus the addition of 125 to the run length. |
| uint8_t key_byte = run_length + 125; |
| rle_data->push_back(key_byte); |
| rle_data->push_back(current_byte); |
| data = data.subspan(run_length); |
| } else { |
| // The run is too small, so keep looking. |
| search_offset += run_length; |
| } |
| } |
| // At this point, there are no more runs, so pack the rest of the data into |
| // the output block. |
| while (search_offset > 0) { |
| // Because uncompressed data runs max out at 128 bytes of data, cap the |
| // uncompressed run at 128 bytes. |
| base::span<const uint8_t> uncompressed_chunk = |
| data.first(std::min<size_t>(search_offset, 128)); |
| // Key byte values of 0..127 mean 1..128 bytes of uncompressed data. |
| uint8_t key_byte = uncompressed_chunk.size() - 1; |
| rle_data->push_back(key_byte); |
| rle_data->insert(rle_data->end(), uncompressed_chunk.begin(), |
| uncompressed_chunk.end()); |
| data = data.subspan(uncompressed_chunk.size()); |
| search_offset -= uncompressed_chunk.size(); |
| } |
| } |
| |
| void IcnsEncoder::AppendBlock(uint32_t type, std::vector<uint8_t> data) { |
| total_block_size_ += data.size() + kBlockHeaderSize; |
| blocks_.emplace_back(type, std::move(data)); |
| } |
| |
| // static |
| bool IcnsEncoder::WriteBlockToFile(base::File& file, const Block& block) { |
| if (!file.WriteAtCurrentPosAndCheck( |
| CreateBlockHeader(block.type, block.data.size()))) |
| return false; |
| if (!file.WriteAtCurrentPosAndCheck(block.data)) |
| return false; |
| return true; |
| } |
| |
| } // namespace web_app |