| // Copyright 2018 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/ui/app_list/md_icon_normalizer.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" |
| |
| // 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 = static_cast<float>(M_PI) / 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) { |
| 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) { |
| 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 (float)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 |
| ? (float)std::sqrt(scale_required / area_scale) |
| : 1; |
| } |
| |
| } // 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) { |
| 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(gfx::ImageSkiaRep(resized, 1)), |
| padding.width(), padding.height()); |
| *bitmap_out = canvas.GetBitmap(); |
| return; |
| } |
| |
| void MaybeResizeAndPadIconForMd(const gfx::Size& required_size_dip, |
| gfx::ImageSkia* icon_out) { |
| 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.width() != 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 |