chromium/chrome/browser/web_applications/os_integration/mac/icns_encoder.cc

// Copyright 2022 The Chromium Authors
// 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/mac/icns_encoder.h"

#include <algorithm>

#include "base/files/file.h"
#include "base/notreached.h"
#include "base/numerics/byte_conversions.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(8u);
  auto [first, second] = base::span(result).split_at<4u>();
  first.copy_from(base::U32ToBigEndian(type));
  second.copy_from(
      base::U32ToBigEndian(base::checked_cast<uint32_t>(data_length + 8u)));
  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(
      kIcnsBlockTypes, bitmap.width(), &IcnsBlockTypes::size);
  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