blob: 68433fcb385dfa423c1f9043d32132d827ce6782 [file] [log] [blame]
// Copyright 2020 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/apps/icon_standardizer.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
namespace apps {
namespace {
constexpr float kCircleOutlineStrokeWidthRatio = 0.1f;
constexpr int kMinimumVisibleAlpha = 40;
constexpr float kCircleShapePixelDifferenceThreshold = 0.01f;
constexpr float kIconScaleToFit = 0.85f;
constexpr float kBackgroundCircleScale = 176.0f / 192.0f;
// Returns the bounding rect for the opaque part of the icon.
gfx::Rect GetVisibleIconBounds(const SkBitmap& bitmap) {
const SkPixmap pixmap = bitmap.pixmap();
bool const nativeColorType = pixmap.colorType() == kN32_SkColorType;
const int width = pixmap.width();
const int height = pixmap.height();
// Overall bounds of the visible icon.
int y_from = -1;
int y_to = -1;
int x_left = width;
int x_right = -1;
// Find bounding rect of the visible icon by going through all pixels one row
// at a time and for each row find the first and the last non-transparent
// pixel.
for (int y = 0; y < height; y++) {
const SkColor* nativeRow =
nativeColorType
? reinterpret_cast<const SkColor*>(bitmap.getAddr32(0, y))
: nullptr;
bool does_row_have_visible_pixels = false;
for (int x = 0; x < width; x++) {
if (SkColorGetA(nativeRow ? nativeRow[x] : pixmap.getColor(x, y)) >
kMinimumVisibleAlpha) {
does_row_have_visible_pixels = true;
x_left = std::min(x_left, x);
break;
}
}
// No visible pixels on this row.
if (!does_row_have_visible_pixels)
continue;
for (int x = width - 1; x > 0; x--) {
if (SkColorGetA(nativeRow ? nativeRow[x] : pixmap.getColor(x, y)) >
kMinimumVisibleAlpha) {
x_right = std::max(x_right, x);
break;
}
}
y_to = y;
if (y_from == -1)
y_from = y;
}
int visible_width = x_right - x_left + 1;
int visible_height = y_to - y_from + 1;
return gfx::Rect(x_left, y_from, visible_width, visible_height);
}
float GetDistanceBetweenPoints(gfx::PointF first_point,
gfx::PointF second_point) {
float x_difference = first_point.x() - second_point.x();
float y_difference = first_point.y() - second_point.y();
return sqrt(x_difference * x_difference + y_difference * y_difference);
}
// Returns the distance for the farthest visible pixel away from the center of
// the icon.
float GetFarthestVisiblePointFromCenter(const SkBitmap& bitmap) {
int width = bitmap.width();
int height = bitmap.height();
const SkPixmap pixmap = bitmap.pixmap();
bool const nativeColorType = pixmap.colorType() == kN32_SkColorType;
gfx::PointF center_point((width - 1) / 2.0f, (height - 1) / 2.0f);
float max_distance = -1.0f;
// Find the farthest visible pixel from the center by going through one row
// at a time and for each row find the first and the last non-transparent
// pixel and calculate its distance from the center.
for (int y = 0; y < height; y++) {
const SkColor* nativeRow =
nativeColorType
? reinterpret_cast<const SkColor*>(bitmap.getAddr32(0, y))
: nullptr;
bool does_row_have_visible_pixels = false;
for (int x = 0; x < width; x++) {
if (SkColorGetA(nativeRow ? nativeRow[x] : pixmap.getColor(x, y)) >
kMinimumVisibleAlpha) {
gfx::PointF current_point(x, y);
max_distance =
std::max(GetDistanceBetweenPoints(current_point, center_point),
max_distance);
does_row_have_visible_pixels = true;
break;
}
}
// No visible pixels on this row.
if (!does_row_have_visible_pixels)
continue;
for (int x = width - 1; x > 0; x--) {
if (SkColorGetA(nativeRow ? nativeRow[x] : pixmap.getColor(x, y)) >
kMinimumVisibleAlpha) {
gfx::PointF current_point(x, y);
max_distance =
std::max(GetDistanceBetweenPoints(current_point, center_point),
max_distance);
break;
}
}
}
return (max_distance == -1.0f) ? (sqrt(width * width * 2)) : max_distance;
}
// Returns whether the shape of the icon is roughly circle shaped.
bool IsIconCircleShaped(const gfx::ImageSkia& image) {
bool is_icon_already_circle_shaped = false;
for (gfx::ImageSkiaRep rep : image.image_reps()) {
SkBitmap bitmap(rep.GetBitmap());
int width = bitmap.width();
int height = bitmap.height();
SkBitmap preview;
preview.allocN32Pixels(width, height);
preview.eraseColor(SK_ColorTRANSPARENT);
// |preview| will be the original icon with all visible pixels colored red.
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
const SkColor* src_color =
reinterpret_cast<SkColor*>(bitmap.getAddr32(0, y));
SkColor* preview_color =
reinterpret_cast<SkColor*>(preview.getAddr32(0, y));
SkColor target_color;
if (SkColorGetA(src_color[x]) < 1) {
target_color = SK_ColorTRANSPARENT;
} else {
target_color = SK_ColorRED;
}
preview_color[x] = target_color;
}
}
gfx::Rect visible_preview_bounds = GetVisibleIconBounds(preview);
float visible_icon_diagonal = std::sqrt(
visible_preview_bounds.height() * visible_preview_bounds.height() +
visible_preview_bounds.width() * visible_preview_bounds.width());
float preview_diagonal = std::sqrt(preview.height() * preview.height() +
preview.width() * preview.width());
float scale = preview_diagonal / visible_icon_diagonal;
// If the visible icon requires too large of a scale, then the icon is small
// enough that it should not be considered circular. This also serves as a
// speculative crash fix by setting an upper limit on |scale| in the case it
// is too large. (crbug.com/1162155)
if (scale >= 1.6f)
return false;
gfx::Size scaled_icon_size =
gfx::ScaleToRoundedSize(rep.pixel_size(), scale);
// To detect a circle shaped icon of any size, resize and scale |preview| so
// the visible icon bounds match the maximum width and height of the bitmap.
const SkBitmap scaled_preview = skia::ImageOperations::Resize(
preview, skia::ImageOperations::RESIZE_BEST, scaled_icon_size.width(),
scaled_icon_size.height());
preview.eraseColor(SK_ColorTRANSPARENT);
SkCanvas canvas1(preview);
canvas1.drawImage(scaled_preview.asImage(),
-visible_preview_bounds.x() * scale,
-visible_preview_bounds.y() * scale);
// Use a canvas to perform XOR and DST_OUT operations, which should
// generate a transparent bitmap for |preview| if the original icon is
// shaped like a circle.
SkCanvas canvas(preview);
SkPaint paint_circle_mask;
paint_circle_mask.setColor(SK_ColorBLUE);
paint_circle_mask.setStyle(SkPaint::kFill_Style);
paint_circle_mask.setAntiAlias(true);
// XOR operation to remove a circle.
paint_circle_mask.setBlendMode(SkBlendMode::kXor);
canvas.drawCircle(SkPoint::Make(width / 2.0f, height / 2.0f), width / 2.0,
paint_circle_mask);
SkPaint paint_outline;
paint_outline.setColor(SK_ColorGREEN);
paint_outline.setStyle(SkPaint::kStroke_Style);
const float outline_stroke_width = width * kCircleOutlineStrokeWidthRatio;
const float radius_offset = outline_stroke_width / 8.0f;
paint_outline.setStrokeWidth(outline_stroke_width);
paint_outline.setAntiAlias(true);
// DST_OUT operation to remove an extra circle outline.
paint_outline.setBlendMode(SkBlendMode::kDstOut);
canvas.drawCircle(SkPoint::Make(width / 2.0f, height / 2.0f),
width / 2.0f + radius_offset, paint_outline);
// Compute the total pixel difference between the circle mask and the
// original icon.
int total_pixel_difference = 0;
for (int y = 0; y < preview.height(); ++y) {
SkColor* src_color = reinterpret_cast<SkColor*>(preview.getAddr32(0, y));
for (int x = 0; x < preview.width(); ++x) {
if (SkColorGetA(src_color[x]) >= kMinimumVisibleAlpha)
total_pixel_difference++;
}
}
float percentage_diff_pixels =
static_cast<float>(total_pixel_difference) / (width * height);
// If the pixel difference between a circle and the original icon is small
// enough, then the icon can be considered circle shaped.
if (percentage_diff_pixels < kCircleShapePixelDifferenceThreshold)
is_icon_already_circle_shaped = true;
}
return is_icon_already_circle_shaped;
}
// Returns an image with equal width and height. If necessary, padding is
// added to ensure the width and height are equal.
gfx::ImageSkia StandardizeSize(const gfx::ImageSkia& image) {
gfx::ImageSkia final_image;
for (gfx::ImageSkiaRep rep : image.image_reps()) {
SkBitmap unscaled_bitmap(rep.GetBitmap());
int width = unscaled_bitmap.width();
int height = unscaled_bitmap.height();
if (width == height)
return image;
int longest_side = std::max(width, height);
SkBitmap final_bitmap;
final_bitmap.allocN32Pixels(longest_side, longest_side);
final_bitmap.eraseColor(SK_ColorTRANSPARENT);
SkCanvas canvas(final_bitmap);
canvas.drawImage(unscaled_bitmap.asImage(), (longest_side - width) / 2,
(longest_side - height) / 2);
final_image.AddRepresentation(gfx::ImageSkiaRep(final_bitmap, rep.scale()));
}
return final_image;
}
} // namespace
gfx::ImageSkia CreateStandardIconImage(const gfx::ImageSkia& image) {
gfx::ImageSkia final_image;
gfx::ImageSkia standard_size_image = apps::StandardizeSize(image);
// If icon is already circle shaped, then return the original image and make
// sure the image is scaled down if its icon size takes up too much space
// within the bitmap.
if (IsIconCircleShaped(standard_size_image)) {
for (gfx::ImageSkiaRep rep : standard_size_image.image_reps()) {
SkBitmap unscaled_bitmap(rep.GetBitmap());
int width = unscaled_bitmap.width();
int height = unscaled_bitmap.height();
SkBitmap final_bitmap;
final_bitmap.allocN32Pixels(width, height);
final_bitmap.eraseColor(SK_ColorTRANSPARENT);
float dis_to_center = GetFarthestVisiblePointFromCenter(unscaled_bitmap);
float icon_to_bitmap_size_ratio = dis_to_center * 2.0f / width;
if (icon_to_bitmap_size_ratio > kBackgroundCircleScale) {
SkCanvas canvas(final_bitmap);
SkPaint paint_icon;
paint_icon.setMaskFilter(nullptr);
paint_icon.setBlendMode(SkBlendMode::kSrcOver);
float icon_scale = kBackgroundCircleScale / icon_to_bitmap_size_ratio;
gfx::Size scaled_icon_size =
gfx::ScaleToRoundedSize(rep.pixel_size(), icon_scale);
const SkBitmap scaled_bitmap = skia::ImageOperations::Resize(
unscaled_bitmap, skia::ImageOperations::RESIZE_BEST,
scaled_icon_size.width(), scaled_icon_size.height());
int target_left = (width - scaled_icon_size.width()) / 2;
int target_top = (height - scaled_icon_size.height()) / 2;
// Draw the scaled down bitmap and add that to the final image.
canvas.drawImage(scaled_bitmap.asImage(), target_left, target_top,
SkSamplingOptions(), &paint_icon);
final_image.AddRepresentation(
gfx::ImageSkiaRep(final_bitmap, rep.scale()));
} else {
// No need to scale down the icon, so just use the |unscaled_bitmap|.
final_image.AddRepresentation(
gfx::ImageSkiaRep(unscaled_bitmap, rep.scale()));
}
}
return final_image;
}
for (gfx::ImageSkiaRep rep : standard_size_image.image_reps()) {
SkBitmap unscaled_bitmap(rep.GetBitmap());
int width = unscaled_bitmap.width();
int height = unscaled_bitmap.height();
SkBitmap final_bitmap;
final_bitmap.allocN32Pixels(width, height);
final_bitmap.eraseColor(SK_ColorTRANSPARENT);
// To draw to |final_bitmap|, create a canvas and draw a circle background
// with an app icon on top;
SkCanvas canvas(final_bitmap);
SkPaint paint_background_circle;
paint_background_circle.setAntiAlias(true);
paint_background_circle.setColor(SK_ColorWHITE);
paint_background_circle.setStyle(SkPaint::kFill_Style);
float circle_diameter = width * kBackgroundCircleScale;
// Draw the background circle.
canvas.drawCircle(SkPoint::Make((width - 1) / 2.0f, (height - 1) / 2.0f),
circle_diameter / 2.0f, paint_background_circle);
float dis_to_center = GetFarthestVisiblePointFromCenter(unscaled_bitmap);
float icon_diameter = dis_to_center * 2.0f;
float target_diameter = circle_diameter * kIconScaleToFit;
// If the icon is too big to fit correctly within the background circle,
// then set |icon_scale| to fit.
float icon_scale = (icon_diameter > target_diameter)
? target_diameter / icon_diameter
: 1.0f;
SkPaint paint_icon;
paint_icon.setMaskFilter(nullptr);
paint_icon.setBlendMode(SkBlendMode::kSrcOver);
if (icon_scale == 1.0f) {
// Draw the unscaled icon on top of the background.
canvas.drawImage(unscaled_bitmap.asImage(), 0, 0, SkSamplingOptions(),
&paint_icon);
} else {
gfx::Size scaled_icon_size =
gfx::ScaleToRoundedSize(rep.pixel_size(), icon_scale);
const SkBitmap scaled_bitmap = skia::ImageOperations::Resize(
unscaled_bitmap, skia::ImageOperations::RESIZE_BEST,
scaled_icon_size.width(), scaled_icon_size.height());
int target_left = (width - scaled_icon_size.width()) / 2;
int target_top = (height - scaled_icon_size.height()) / 2;
// Draw the scaled icon on top of the background.
canvas.drawImage(scaled_bitmap.asImage(), target_left, target_top,
SkSamplingOptions(), &paint_icon);
}
final_image.AddRepresentation(gfx::ImageSkiaRep(final_bitmap, rep.scale()));
}
return final_image;
}
} // namespace apps