blob: 4fda3913e3f2c9dfbd46b5a97d3d28215e32d5ac [file] [log] [blame]
// Copyright 2024 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/shortcuts/icon_badging.h"
#include <algorithm>
#include <cmath>
#include <vector>
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/containers/enum_set.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/flat_map.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/version_info/channel.h"
#include "build/branding_buildflags.h"
#include "build/buildflag.h"
#include "chrome/common/channel_info.h"
#include "chrome/grit/chrome_unscaled_resources.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkImage.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_family.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
namespace shortcuts {
namespace {
enum class ShortcutSize {
k16,
k32,
k48,
k128,
k256,
k512,
kMaxValue = k512
};
enum class BadgeSize {
k8,
k16,
k24,
k64,
k128,
kMaxValue = k128
};
// Returns the icon sizes needed for shortcut creation on the desktop. k32 is
// needed for the icon in the Create Shortcut dialog.
#if BUILDFLAG(IS_MAC)
constexpr ShortcutSize kSizesNeededForShortcutCreation[] = {
ShortcutSize::k16, ShortcutSize::k32, ShortcutSize::k128,
ShortcutSize::k256, ShortcutSize::k512};
#elif BUILDFLAG(IS_LINUX)
constexpr ShortcutSize kSizesNeededForShortcutCreation[] = {ShortcutSize::k32,
ShortcutSize::k128};
#elif BUILDFLAG(IS_WIN)
constexpr ShortcutSize kSizesNeededForShortcutCreation[] = {
ShortcutSize::k16, ShortcutSize::k32, ShortcutSize::k48,
ShortcutSize::k256};
#endif
int ToInt(ShortcutSize size) {
switch (size) {
case ShortcutSize::k16:
return 16;
case ShortcutSize::k32:
return 32;
case ShortcutSize::k48:
return 48;
case ShortcutSize::k128:
return 128;
case ShortcutSize::k256:
return 256;
case ShortcutSize::k512:
return 512;
}
}
int ToInt(BadgeSize size) {
switch (size) {
case BadgeSize::k8:
return 8;
case BadgeSize::k16:
return 16;
case BadgeSize::k24:
return 24;
case BadgeSize::k64:
return 64;
case BadgeSize::k128:
return 128;
}
}
// For icon sizes > 128, use a badge size that is 1/4, else use a badge size
// that is 1/2.
BadgeSize GetBadgeSizeFromShortcutSize(ShortcutSize icon_size) {
switch (icon_size) {
case ShortcutSize::k16:
return BadgeSize::k8;
case ShortcutSize::k32:
return BadgeSize::k16;
case ShortcutSize::k48:
return BadgeSize::k24;
case ShortcutSize::k128:
return BadgeSize::k64;
case ShortcutSize::k256:
return BadgeSize::k64;
case ShortcutSize::k512:
return BadgeSize::k128;
}
}
constexpr int resource_map_size = static_cast<int>(BadgeSize::kMaxValue) + 1;
using SizeToResourceMap = base::fixed_flat_map<BadgeSize, int, 5>;
// At very low pixels < 16, there is probably no need to recreate the
// resource files necessary for these icons. Resizing them instead would
// work just as better, without the need to increase the chrome binary
// size.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
constexpr SizeToResourceMap kStableResourceMap =
base::MakeFixedFlatMap<BadgeSize, int>(
{{BadgeSize::k8, IDR_PRODUCT_LOGO_16_STABLE_SHORTCUTS},
{BadgeSize::k16, IDR_PRODUCT_LOGO_16_STABLE_SHORTCUTS},
{BadgeSize::k24, IDR_PRODUCT_LOGO_24_STABLE_SHORTCUTS},
{BadgeSize::k64, IDR_PRODUCT_LOGO_64_STABLE_SHORTCUTS},
{BadgeSize::k128, IDR_PRODUCT_LOGO_128_STABLE_SHORTCUTS}});
constexpr SizeToResourceMap kCanaryResourceMap =
base::MakeFixedFlatMap<BadgeSize, int>(
{{BadgeSize::k8, IDR_PRODUCT_LOGO_CANARY_16_SHORTCUTS},
{BadgeSize::k16, IDR_PRODUCT_LOGO_CANARY_16_SHORTCUTS},
{BadgeSize::k24, IDR_PRODUCT_LOGO_CANARY_24_SHORTCUTS},
{BadgeSize::k64, IDR_PRODUCT_LOGO_CANARY_64_SHORTCUTS},
{BadgeSize::k128, IDR_PRODUCT_LOGO_CANARY_128_SHORTCUTS}});
constexpr SizeToResourceMap kBetaResourceMap =
base::MakeFixedFlatMap<BadgeSize, int>(
{{BadgeSize::k8, IDR_PRODUCT_LOGO_BETA_16_SHORTCUTS},
{BadgeSize::k16, IDR_PRODUCT_LOGO_BETA_16_SHORTCUTS},
{BadgeSize::k24, IDR_PRODUCT_LOGO_BETA_24_SHORTCUTS},
{BadgeSize::k64, IDR_PRODUCT_LOGO_BETA_64_SHORTCUTS},
{BadgeSize::k128, IDR_PRODUCT_LOGO_BETA_128_SHORTCUTS}});
constexpr SizeToResourceMap kDevResourceMap =
base::MakeFixedFlatMap<BadgeSize, int>(
{{BadgeSize::k8, IDR_PRODUCT_LOGO_DEV_16_SHORTCUTS},
{BadgeSize::k16, IDR_PRODUCT_LOGO_DEV_16_SHORTCUTS},
{BadgeSize::k24, IDR_PRODUCT_LOGO_DEV_24_SHORTCUTS},
{BadgeSize::k64, IDR_PRODUCT_LOGO_DEV_64_SHORTCUTS},
{BadgeSize::k128, IDR_PRODUCT_LOGO_DEV_128_SHORTCUTS}});
static_assert(static_cast<int>(kStableResourceMap.size()) == resource_map_size,
"Ensure icon resources are filled for all entries in BadgeSize "
"for stable resource map");
static_assert(static_cast<int>(kCanaryResourceMap.size()) == resource_map_size,
"Ensure icon resources are filled for all entries in BadgeSize "
"for canary resource map");
static_assert(static_cast<int>(kBetaResourceMap.size()) == resource_map_size,
"Ensure icon resources are filled for all entries in BadgeSize "
"for beta resource map");
static_assert(static_cast<int>(kDevResourceMap.size()) == resource_map_size,
"Ensure icon resources are filled for all entries in BadgeSize "
"for dev resource map");
#elif BUILDFLAG(GOOGLE_CHROME_FOR_TESTING_BRANDING)
constexpr SizeToResourceMap kChromeForTestingBrandedResourceMap =
base::MakeFixedFlatMap<BadgeSize, int>(
{{BadgeSize::k8, IDR_PRODUCT_LOGO_16_CFT_SHORTCUTS},
{BadgeSize::k16, IDR_PRODUCT_LOGO_16_CFT_SHORTCUTS},
{BadgeSize::k24, IDR_PRODUCT_LOGO_24_CFT_SHORTCUTS},
{BadgeSize::k64, IDR_PRODUCT_LOGO_64_CFT_SHORTCUTS},
{BadgeSize::k128, IDR_PRODUCT_LOGO_128_CFT_SHORTCUTS}});
static_assert(static_cast<int>(kChromeForTestingBrandedResourceMap.size()) ==
resource_map_size,
"Ensure icon resources are filled for all entries in BadgeSize "
"for Chrome for testing resource map");
#else
constexpr SizeToResourceMap kChromiumResourceMap =
base::MakeFixedFlatMap<BadgeSize, int>(
{{BadgeSize::k8, IDR_PRODUCT_LOGO_16_SHORTCUTS},
{BadgeSize::k16, IDR_PRODUCT_LOGO_16_SHORTCUTS},
{BadgeSize::k24, IDR_PRODUCT_LOGO_24_SHORTCUTS},
{BadgeSize::k64, IDR_PRODUCT_LOGO_64_SHORTCUTS},
{BadgeSize::k128, IDR_PRODUCT_LOGO_128_SHORTCUTS}});
static_assert(static_cast<int>(kChromiumResourceMap.size()) ==
resource_map_size,
"Ensure icon resources are filled for all entries in BadgeSize "
"for chromium resource map");
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
SizeToResourceMap GetResourceMapForCurrentChannel() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
switch (chrome::GetChannel()) {
// |version_info::Channel::DEFAULT| is seen on local builds with
// is_chrome_branded = true.
case version_info::Channel::DEFAULT:
case version_info::Channel::STABLE:
return kStableResourceMap;
case version_info::Channel::CANARY:
return kCanaryResourceMap;
case version_info::Channel::DEV:
return kDevResourceMap;
case version_info::Channel::BETA:
return kBetaResourceMap;
}
#elif BUILDFLAG(GOOGLE_CHROME_FOR_TESTING_BRANDING)
return kChromeForTestingBrandedResourceMap;
#else
return kChromiumResourceMap;
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
}
gfx::ImageSkia GetMaskForBadging(ShortcutSize icon_size) {
BadgeSize badge_size = GetBadgeSizeFromShortcutSize(icon_size);
gfx::ImageSkia* image =
ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
GetResourceMapForCurrentChannel().at(badge_size));
CHECK(image);
// TODO(crbug.com/337998776): Update if assets are sent by UX.
const float kWhiteMaskFactor = 1.75f;
// Set white mask for the loaded icon.
gfx::ImageSkia masked_badge =
gfx::ImageSkiaOperations::CreateImageWithCircleBackground(
image->width() / kWhiteMaskFactor, SK_ColorWHITE, *image);
int badge_size_int = ToInt(badge_size);
// Maintain the badging ratio of 1/4th for sizes < 128, and 1/8th
// for sizes greater. Adding the white background after loading the resource
// will likely throw that ratio off.
gfx::ImageSkia resized_post_masking =
gfx::ImageSkiaOperations::CreateResizedImage(
masked_badge, skia::ImageOperations::ResizeMethod::RESIZE_LANCZOS3,
gfx::Size(badge_size_int, badge_size_int));
return resized_post_masking;
}
int FindClosestIconSizeToUse(const std::vector<int>& sorted_icons,
int size_to_use) {
auto closest_size_iter =
std::lower_bound(sorted_icons.begin(), sorted_icons.end(), size_to_use);
// Iterator out of bounds, can happen if the input icon size is too large,
// like say 1000.
if (closest_size_iter == sorted_icons.end()) {
closest_size_iter = sorted_icons.end() - 1;
} else if (closest_size_iter != sorted_icons.begin()) {
// Compare with the iterator found and the one just before, and choose the
// size that is closer. This helps reduce the pixelation when the icon gets
// resized ultimately.
int size_before = *(closest_size_iter - 1);
int size_after = *(closest_size_iter);
if (std::fabs(size_to_use - size_before) <
std::fabs(size_to_use - size_after)) {
--closest_size_iter;
}
}
return *closest_size_iter;
}
using ShortcutSizes =
base::EnumSet<ShortcutSize, ShortcutSize::k16, ShortcutSize::k512>;
} // namespace
gfx::ImageFamily ApplyProductLogoBadgeToIcons(std::vector<SkBitmap> icons) {
gfx::ImageFamily badged_icons;
CHECK(!icons.empty());
base::flat_map<int, SkBitmap> sorted_icons;
std::vector<int> icon_sizes;
for (const auto& icon : icons) {
sorted_icons.insert_or_assign(icon.width(), icon);
icon_sizes.push_back(icon.width());
}
std::sort(icon_sizes.begin(), icon_sizes.end());
for (const ShortcutSize needed_size : kSizesNeededForShortcutCreation) {
int icon_size = ToInt(needed_size);
int size_to_use = FindClosestIconSizeToUse(icon_sizes, icon_size);
SkBitmap bitmap_to_use = sorted_icons.at(size_to_use);
// Resize the current bitmap to the needed_size, so that the properly
// selected product logo does not get pixellated.
gfx::ImageSkia resized_to_fit =
gfx::ImageSkiaOperations::CreateResizedImage(
gfx::ImageSkia::CreateFrom1xBitmap(bitmap_to_use),
skia::ImageOperations::ResizeMethod::RESIZE_BEST,
gfx::Size(icon_size, icon_size));
gfx::ImageSkia masked_badge = GetMaskForBadging(needed_size);
// Apply the masked product logo to the bitmaps.
gfx::ImageSkia badged_icon = gfx::ImageSkiaOperations::CreateIconWithBadge(
resized_to_fit, masked_badge);
// Doing this allows the returned gfx::ImageFamily to be passed across
// multiple sequences, like when this is passed to the ThreadPool during
// shortcut creation at the OS level.
badged_icon.MakeThreadSafe();
badged_icons.Add(badged_icon);
}
return badged_icons;
}
} // namespace shortcuts