chromium/chrome/browser/ash/app_list/md_icon_normalizer.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ash/app_list/md_icon_normalizer.h"

#include <algorithm>
#include <cmath>
#include <numbers>
#include <utility>
#include <vector>

#include "base/trace_event/trace_event.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/image/image_skia_rep.h"

// The implementation is copied and adapted from the Android Launcher.
// See com.android.launcher3.graphics.IconNormalizer.java in the Android source.

namespace app_list {

namespace {

// No normalization to be attempted for icons smaller than this size.
constexpr int kMinIconSize = 32;

// Ratio of icon visible area to full icon size for a square shaped icon.
constexpr float kMaxSquareAreaFactor = 361.0f / 576;

// Ratio of icon visible area to full icon size for a circular shaped icon.
constexpr float kMaxCircleAreaFactor = 380.0f / 576;

constexpr float kCircleAreaByRect = std::numbers::pi_v<float> / 4;

// Slope used to calculate icon visible area to full icon size for any generic
// shaped icon.
constexpr float kLinearScaleSlope =
    (kMaxCircleAreaFactor - kMaxSquareAreaFactor) / (1 - kCircleAreaByRect);

constexpr int kMaxShadowAlpha = 40;

void ConvertToConvexArray(std::vector<float>* x_coord,
                          int direction,
                          int y_from,
                          int y_to) {
  TRACE_EVENT0("ui", "app_list::ConvertToConvexArray");
  std::vector<float> angles(y_to - y_from);

  int y_last = -1;  // Last valid y coordinate which didn't have a missing value

  float last_angle;

  for (int i = y_from + 1; i <= y_to; i++) {
    if ((*x_coord)[i] <= -1)
      continue;

    int start;

    if (y_last == -1) {
      start = y_from;
    } else {
      float current_angle = ((*x_coord)[i] - (*x_coord)[y_last]) / (i - y_last);
      start = y_last;
      // If this position creates a concave angle, keep moving up until we find
      // a position which creates a convex angle.
      if ((current_angle - last_angle) * direction < 0) {
        while (start > y_from) {
          start--;
          current_angle = ((*x_coord)[i] - (*x_coord)[start]) / (i - start);
          if ((current_angle - angles[start - y_from]) * direction >= 0)
            break;
        }
      }
    }

    // Reset from last check
    last_angle = ((*x_coord)[i] - (*x_coord)[start]) / (i - start);
    // Update all the points from start.
    for (int j = start; j < i; j++) {
      angles[j - y_from] = last_angle;
      (*x_coord)[j] = (*x_coord)[start] + last_angle * (j - start);
    }
    y_last = i;
  }
}

float GetMdIconScale(const SkBitmap& bitmap) {
  TRACE_EVENT0("ui", "app_list::GetMdIconScale");
  const SkPixmap pixmap = bitmap.pixmap();

  // In the absence of alpha information, assume that the icon is a fully opaque
  // square and scale accordingly.
  if (pixmap.alphaType() == kUnknown_SkAlphaType ||
      pixmap.alphaType() == kOpaque_SkAlphaType) {
    return std::sqrt(kMaxSquareAreaFactor);
  }

  bool const nativeColorType = pixmap.colorType() == kN32_SkColorType;

  const int width = pixmap.width();
  const int height = pixmap.height();

  // If the icon is too small, no scaling makes sense.
  if (std::min(width, height) < kMinIconSize)
    return 1;

  std::vector<float> border_left(height, -1);
  std::vector<float> border_right(height, -1);

  // Overall bounds of the visible icon.
  int y_from = -1;
  int y_to = -1;
  int x_left = width;
  int x_right = -1;

  // Create border by going through all pixels one row at a time and for each
  // row find the first and the last non-transparent pixel. Set those values to
  // border_left and border_right and use -1 if there are no visible pixel in
  // the row.

  for (int y = 0; y < height; y++) {
    const SkColor* nativeRow =
        nativeColorType
            ? reinterpret_cast<const SkColor*>(bitmap.getAddr32(0, y))
            : nullptr;

    for (int x = 0; x < width; x++) {
      if (SkColorGetA(nativeRow ? nativeRow[x] : pixmap.getColor(x, y)) >
          kMaxShadowAlpha) {
        border_left[y] = x;
        x_left = std::min(x_left, x);
        break;
      }
    }

    // No visible pixels on this row.
    if (border_left[y] == -1)
      continue;

    for (int x = width - 1; x > 0; x--) {
      if (SkColorGetA(nativeRow ? nativeRow[x] : pixmap.getColor(x, y)) >
          kMaxShadowAlpha) {
        border_right[y] = x;
        x_right = std::max(x_right, x);
        break;
      }
    }

    y_to = y;
    if (y_from == -1)
      y_from = y;
  }

  if (y_from == -1) {
    // No valid pixels found. Do not scale.
    return 1;
  }

  ConvertToConvexArray(&border_left, 1, y_from, y_to);
  ConvertToConvexArray(&border_right, -1, y_from, y_to);

  // Area of the convex hull
  float area = 0;
  for (int y = 0; y < height; y++) {
    if (border_left[y] <= -1)
      continue;
    area += border_right[y] - border_left[y] + 1;
  }

  // Area of the rectangle required to fit the convex hull
  float rect_area = (y_to + 1 - y_from) * (x_right + 1 - x_left);
  float hull_by_rect = area / rect_area;

  float scale_required;
  if (hull_by_rect < kCircleAreaByRect) {
    scale_required = kMaxCircleAreaFactor;
  } else {
    scale_required =
        kMaxSquareAreaFactor + kLinearScaleSlope * (1 - hull_by_rect);
  }

  float area_scale = area / (width * height);
  // Use sqrt of the final ratio as the image is scaled across both width and
  // height.
  return area_scale > scale_required ? std::sqrt(scale_required / area_scale)
                                     : 1.0f;
}

}  // namespace

gfx::Size GetMdIconPadding(const SkBitmap& bitmap,
                           const gfx::Size& required_size) {
  const float scale = GetMdIconScale(bitmap);
  const float padding_factor = (1 - scale) / 2;
  return gfx::Size(
      static_cast<int>(required_size.width() * padding_factor + 0.5),
      static_cast<int>(required_size.height() * padding_factor + 0.5));
}

void MaybeResizeAndPad(const gfx::Size& required_size,
                       const gfx::Size& padding,
                       SkBitmap* bitmap_out) {
  TRACE_EVENT0("ui", "app_list::MaybeResizeAndPad");
  if (!padding.width() && !padding.height() &&
      required_size.width() == bitmap_out->width() &&
      required_size.height() == bitmap_out->height()) {
    // Neither padding no resizing required, do nothing.
    return;
  }

  const int resized_width = required_size.width() - 2 * padding.width();
  const int resized_height = required_size.height() - 2 * padding.height();
  const SkBitmap resized = skia::ImageOperations::Resize(
      *bitmap_out, skia::ImageOperations::RESIZE_LANCZOS3, resized_width,
      resized_height);
  if (!padding.width() && !padding.height()) {
    // No padding required, return the resized bitmap.
    *bitmap_out = resized;
    return;
  }

  // Add padding.
  gfx::Canvas canvas(required_size, 1, /*transparent=*/false);
  canvas.DrawImageInt(gfx::ImageSkia::CreateFromBitmap(resized, 1),
                      padding.width(), padding.height());
  *bitmap_out = canvas.GetBitmap();
  return;
}

void MaybeResizeAndPadIconForMd(const gfx::Size& required_size_dip,
                                gfx::ImageSkia* icon_out) {
  TRACE_EVENT0("ui", "app_list::MaybeResizeAndPadIconForMd");
  bool transformation_required = false;

  // First pass over representations, collect transformation parameters.
  std::vector<std::pair<gfx::Size, gfx::Size>> params;
  for (gfx::ImageSkiaRep rep : icon_out->image_reps()) {
    const SkBitmap& bitmap = rep.GetBitmap();

    gfx::Size required_size_px(
        static_cast<int>(required_size_dip.width() * rep.scale() + 0.5),
        static_cast<int>(required_size_dip.height() * rep.scale() + 0.5));

    const gfx::Size padding_px(GetMdIconPadding(bitmap, required_size_px));

    params.push_back(std::make_pair(required_size_px, padding_px));

    if (required_size_px.width() != bitmap.width() ||
        required_size_px.height() != bitmap.height() ||
        padding_px.width() != 0 || padding_px.height() != 0) {
      transformation_required = true;
    }
  }

  if (!transformation_required)
    return;

  // Second pass over representations, apply transformations.
  gfx::ImageSkia transformed;
  int i = 0;
  for (gfx::ImageSkiaRep rep : icon_out->image_reps()) {
    SkBitmap bitmap = rep.GetBitmap();
    auto param = params[i++];
    MaybeResizeAndPad(param.first, param.second, &bitmap);
    transformed.AddRepresentation(gfx::ImageSkiaRep(bitmap, rep.scale()));
  }
  *icon_out = transformed;
}

float GetMdIconScaleForTest(const SkBitmap& icon) {
  return GetMdIconScale(icon);
}

}  // namespace app_list