blob: be0918f92bdae3a1aba7bd77e2e526a025b4ca7e [file] [log] [blame]
// 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/search/ntp_icon_source.h"
#include <stddef.h>
#include <algorithm>
#include <cmath>
#include "base/callback.h"
#include "base/memory/ref_counted_memory.h"
#include "base/sha1.h"
#include "base/strings/string_number_conversions.h"
#include "build/build_config.h"
#include "cc/paint/skia_paint_canvas.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/history/top_sites_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/instant_io_context.h"
#include "chrome/browser/search/suggestions/image_decoder_impl.h"
#include "chrome/browser/search/suggestions/suggestions_service_factory.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/platform_locale_settings.h"
#include "components/data_use_measurement/core/data_use_user_data.h"
#include "components/favicon/core/fallback_url_util.h"
#include "components/favicon/core/favicon_service.h"
#include "components/favicon_base/favicon_types.h"
#include "components/history/core/browser/top_sites.h"
#include "components/image_fetcher/core/image_fetcher_impl.h"
#include "components/suggestions/proto/suggestions.pb.h"
#include "components/suggestions/suggestions_service.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/escape.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/native_theme/native_theme.h"
#include "url/gurl.h"
namespace {
// The color of the letter drawn for a fallback icon. Changing this may require
// changing the algorithm in RenderIconBitmap() that guarantees contrast.
constexpr SkColor kFallbackIconLetterColor = SK_ColorWHITE;
// Delimiter in the url that looks for the size specification.
const char kSizeParameter[] = "size/";
// Delimiter in the url for dark mode specification.
const char kDarkModeParameter[] = "dark/";
// Size of the icon background (gray circle), in dp.
const int kIconSizeDip = 48;
// Size of the favicon fallback (letter + colored circle), in dp.
const int kFallbackSizeDip = 32;
// URL to the server favicon service. "alt=404" means the service will return a
// 404 if an icon can't be found.
const char kServerFaviconURL[] =
// Used to parse the specification from the path.
struct ParsedNtpIconPath {
// The URL for which the icon is being requested.
GURL url;
// The size of the requested icon in dip.
int size_in_dip = 0;
// The device scale factor of the requested icon.
float device_scale_factor = 1.0;
// Returns true if |search| is a substring of |path| which starts at
// |start_index|.
bool HasSubstringAt(const std::string& path,
size_t start_index,
const std::string& search) {
return, search.length(), search) == 0;
// Parses the path after chrome-search://ntpicon/. Example path is
// "size/24@2x/".
const ParsedNtpIconPath ParseNtpIconPath(const std::string& path) {
ParsedNtpIconPath parsed;
parsed.url = GURL();
parsed.size_in_dip = gfx::kFaviconSize;
parsed.device_scale_factor = 1.0f;
if (path.empty())
return parsed;
// Size specification has to be present.
size_t parsed_index = 0;
if (!HasSubstringAt(path, parsed_index, kSizeParameter))
return parsed;
parsed_index += strlen(kSizeParameter);
size_t slash = path.find("/", parsed_index);
if (slash == std::string::npos)
return parsed;
// Parse the size spec (e.g. "24@2x")
size_t scale_delimiter = path.find("@", parsed_index);
std::string size_str =
path.substr(parsed_index, scale_delimiter - parsed_index);
std::string scale_str =
path.substr(scale_delimiter + 1, slash - scale_delimiter - 1);
if (!base::StringToInt(size_str, &parsed.size_in_dip))
return parsed;
if (!scale_str.empty())
webui::ParseScaleFactor(scale_str, &parsed.device_scale_factor);
parsed_index = slash + 1;
// Parse the dark mode spec (e.g. "dark"), if available. The value is not
// used, but is required to generate a new icon for dark mode.
if (HasSubstringAt(path, parsed_index, kDarkModeParameter)) {
slash = path.find("/", parsed_index);
if (slash == std::string::npos)
return parsed;
parsed_index = slash + 1;
parsed.url = GURL(path.substr(parsed_index));
return parsed;
// Draws a circle of a given |size| and |offset| in the |canvas| and fills it
// with |background_color|.
void DrawCircleInCanvas(gfx::Canvas* canvas,
int size,
int offset,
SkColor background_color) {
cc::PaintFlags flags;
int corner_radius = static_cast<int>(size * 0.5 + 0.5);
canvas->DrawRoundRect(gfx::Rect(offset, offset, size, size), corner_radius,
// Will paint the appropriate letter in the center of specified |canvas| of
// given |size|.
void DrawFallbackIconLetter(const GURL& icon_url,
int size,
gfx::Canvas* canvas) {
// Get the appropriate letter to draw, then eventually draw it.
base::string16 icon_text = favicon::GetFallbackIconText(icon_url);
if (icon_text.empty())
const double kDefaultFontSizeRatio = 0.34;
int font_size = static_cast<int>(size * kDefaultFontSizeRatio);
if (font_size <= 0)
gfx::Font::Weight font_weight = gfx::Font::Weight::NORMAL;
#if defined(OS_WIN)
font_weight = gfx::Font::Weight::SEMIBOLD;
// TODO( Adjust the text color according to the background
// color.
gfx::Font::NORMAL, font_size, font_weight),
kFallbackIconLetterColor, gfx::Rect(0, 0, size, size),
// Will draw |bitmap| in the center of the |canvas| of a given |size|.
// |bitmap| keeps its size.
void DrawFavicon(const SkBitmap& bitmap, gfx::Canvas* canvas, int size) {
int x_origin = (size - bitmap.width()) / 2;
int y_origin = (size - bitmap.height()) / 2;
canvas->DrawImageInt(gfx::ImageSkia(gfx::ImageSkiaRep(bitmap, /*scale=*/1.0)),
x_origin, y_origin);
// Returns a color that based on the hash of |icon_url|'s origin.
SkColor GetBackgroundColorForUrl(const GURL& icon_url) {
if (!icon_url.is_valid())
return SK_ColorGRAY;
unsigned char hash[20];
const std::string origin = icon_url.GetOrigin().spec();
base::SHA1HashBytes(reinterpret_cast<const unsigned char*>(origin.c_str()),
origin.size(), hash);
return SkColorSetRGB(hash[0], hash[1], hash[2]);
// For the given |icon_url|, will render |favicon| within a gray, circular
// background. If |favicon| is not specifed, will use a colored circular
// monogram instead.
std::vector<unsigned char> RenderIconBitmap(const GURL& icon_url,
const SkBitmap& favicon,
int icon_size,
int fallback_size) {
SkBitmap bitmap;
bitmap.allocN32Pixels(icon_size, icon_size, false);
cc::SkiaPaintCanvas paint_canvas(bitmap);
gfx::Canvas canvas(&paint_canvas, 1.f);
canvas.DrawColor(SK_ColorTRANSPARENT, SkBlendMode::kSrc);
// Draw the gray background.
SkColor favicon_bg =
? gfx::kGoogleGrey900
: gfx::kGoogleGrey100;
DrawCircleInCanvas(&canvas, icon_size, /*offset=*/0,
DrawFavicon(favicon, &canvas, icon_size);
// If necessary, draw the colored fallback monogram.
if (favicon.empty()) {
SkColor fallback_color = color_utils::GetColorWithMinimumContrast(
GetBackgroundColorForUrl(icon_url), kFallbackIconLetterColor);
int offset = (icon_size - fallback_size) / 2;
DrawCircleInCanvas(&canvas, fallback_size, offset, fallback_color);
DrawFallbackIconLetter(icon_url, icon_size, &canvas);
std::vector<unsigned char> bitmap_data;
bool result = gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, false, &bitmap_data);
return bitmap_data;
} // namespace
struct NtpIconSource::NtpIconRequest {
NtpIconRequest(const content::URLDataSource::GotDataCallback& cb,
const GURL& path,
int icon_size_in_pixels,
float scale)
: callback(cb),
device_scale_factor(scale) {}
NtpIconRequest(const NtpIconRequest& other) = default;
~NtpIconRequest() {}
content::URLDataSource::GotDataCallback callback;
GURL path;
int icon_size_in_pixels;
float device_scale_factor;
NtpIconSource::NtpIconSource(Profile* profile)
: profile_(profile),
weak_ptr_factory_(this) {
NtpIconSource::~NtpIconSource() = default;
std::string NtpIconSource::GetSource() const {
return chrome::kChromeUINewTabIconHost;
void NtpIconSource::StartDataRequest(
const std::string& path,
const content::ResourceRequestInfo::WebContentsGetter& wc_getter,
const content::URLDataSource::GotDataCallback& callback) {
favicon::FaviconService* favicon_service =
const ParsedNtpIconPath parsed = ParseNtpIconPath(path);
if (parsed.url.is_valid()) {
int icon_size_in_pixels =
std::ceil(parsed.size_in_dip * parsed.device_scale_factor);
NtpIconRequest request(callback, parsed.url, icon_size_in_pixels,
// Check if the requested URL is part of the prepopulated pages (currently,
// only the Web Store).
scoped_refptr<history::TopSites> top_sites =
if (top_sites) {
for (const auto& prepopulated_page : top_sites->GetPrepopulatedPages()) {
if (parsed.url == prepopulated_page.most_visited.url) {
gfx::Image& image =
// Resize as necessary.
gfx::Size target_size(icon_size_in_pixels, icon_size_in_pixels);
if (!image.IsEmpty() && image.Size() != target_size) {
gfx::ImageSkia resized_image =
image.AsImageSkia(), skia::ImageOperations::RESIZE_BEST,
} else {
ReturnRenderedIconForRequest(request, image.AsBitmap());
// This will query for a local favicon. If not found, will take alternative
// action in OnLocalFaviconAvailable.
const bool fallback_to_host = true;
parsed.url, {favicon_base::IconType::kFavicon}, icon_size_in_pixels,
weak_ptr_factory_.GetWeakPtr(), request),
} else {
std::string NtpIconSource::GetMimeType(const std::string&) const {
// NOTE: this may not always be correct for all possible types that this
// source will serve. Seems to work fine, however.
return "image/png";
bool NtpIconSource::ShouldServiceRequest(
const GURL& url,
content::ResourceContext* resource_context,
int render_process_id) const {
if (url.SchemeIs(chrome::kChromeSearchScheme)) {
return InstantIOContext::ShouldServiceRequest(url, resource_context,
return URLDataSource::ShouldServiceRequest(url, resource_context,
void NtpIconSource::OnLocalFaviconAvailable(
const NtpIconRequest& request,
const favicon_base::FaviconRawBitmapResult& bitmap_result) {
if (bitmap_result.is_valid()) {
// A local favicon was found. Decode it to an SkBitmap so it can eventually
// be passed as valid image data to RenderIconBitmap.
SkBitmap bitmap;
bool result =
bitmap_result.bitmap_data.get()->size(), &bitmap);
ReturnRenderedIconForRequest(request, bitmap);
} else {
// Since a local favicon was not found, attempt to fetch a server icon if
// the url is known to the server (this last check is important to avoid
// leaking private history to the server).
bool NtpIconSource::IsRequestedUrlInServerSuggestions(const GURL& url) {
suggestions::SuggestionsService* suggestions_service =
if (!suggestions_service)
return false;
suggestions::SuggestionsProfile profile =
auto position =
std::find_if(profile.suggestions().begin(), profile.suggestions().end(),
[url](const suggestions::ChromeSuggestion& suggestion) {
return suggestion.url() == url.spec();
return position != profile.suggestions().end();
void NtpIconSource::RequestServerFavicon(const NtpIconRequest& request) {
// Only fetch a server icon if the page url is known to the server. This check
// is important to avoid leaking private history to the server.
const GURL server_favicon_url =
GURL(base::StringPrintf(kServerFaviconURL, request.path.spec().c_str()));
if (!server_favicon_url.is_valid() ||
!IsRequestedUrlInServerSuggestions(request.path)) {
ReturnRenderedIconForRequest(request, SkBitmap());
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("ntp_icon_source", R"(
semantics {
sender: "NTP Icon Source"
"Retrieves icons for site suggestions based on the user's browsing "
"history, for use e.g. on the New Tab page."
"Triggered when an icon for a suggestion is required (e.g. on "
"the New Tab page), no local icon is available and the URL is known "
"to the server (hence no private information is revealed)."
data: "The URL for which to retrieve an icon."
policy {
cookies_allowed: NO
"Users cannot disable this feature. The feature is enabled by "
policy_exception_justification: "Not implemented."
gfx::Size(request.icon_size_in_pixels, request.icon_size_in_pixels));
/*id=*/std::string(), server_favicon_url,
weak_ptr_factory_.GetWeakPtr(), request),
void NtpIconSource::OnServerFaviconAvailable(
const NtpIconRequest& request,
const std::string& id,
const gfx::Image& fetched_image,
const image_fetcher::RequestMetadata& metadata) {
// If a server icon was not found, |fetched_bitmap| will be empty and a
// fallback icon will be eventually drawn.
SkBitmap fetched_bitmap = fetched_image.AsBitmap();
if (!fetched_bitmap.empty()) {
// The received server icon bitmap may still be bigger than our desired
// size, so resize it.
fetched_bitmap = skia::ImageOperations::Resize(
fetched_bitmap, skia::ImageOperations::RESIZE_LANCZOS3,
request.icon_size_in_pixels, request.icon_size_in_pixels);
ReturnRenderedIconForRequest(request, fetched_bitmap);
void NtpIconSource::ReturnRenderedIconForRequest(const NtpIconRequest& request,
const SkBitmap& bitmap) {
// Only use even pixel sizes to avoid issues when centering the fallback
// monogram.
int desired_overall_size_in_pixel =
std::round(kIconSizeDip * request.device_scale_factor * 0.5) * 2.0;
int desired_fallback_size_in_pixel =
std::round(kFallbackSizeDip * request.device_scale_factor * 0.5) * 2.0;
std::vector<unsigned char> bitmap_data =
RenderIconBitmap(request.path, bitmap, desired_overall_size_in_pixel,