1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
|
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Handles the download of the search engine favicons.
For all search engines referenced in template_url_prepopulate_data.cc,
downloads their Favicon, scales it and puts it as resource into the repository
for display, e.g. in the search engine choice UI and settings.
This should be run whenever template_url_prepopulate_data.cc changes the list of
search engines used per country, or whenever prepopulated_engines.json changes
a favicon.
To run, `apt-get install python3-commentjson`, then
`python3 tools/search_engine_choice/generate_search_engine_icons.py`.
"""
import hashlib
import os
import re
import shutil
import sys
import commentjson
import requests
def get_image_hash(image_path):
"""Gets the hash of the image that's passed as argument.
This is needed to check whether the downloaded image was already added in the
repo or not.
Args:
image_path: The path of the image for which we want to get the hash.
Returns:
The hash of the image that's passed as argument.
"""
with open(image_path, 'rb') as image:
return hashlib.sha256(image.read()).hexdigest()
def keyword_to_identifer(keyword):
"""Sanitized keyword to be used as identifier.
Replaces characters we find in prepopulates_engines.json's keyword field into
ones that are valid in file names and variable names.
Args:
keyword: the keyword string as in the json file.
Returns:
The keyword string with characters replaced that don't work in a variable or
file name.
"""
return keyword.replace('.', '_').replace('-', '_')
def populate_used_engines():
"""Populates the `used_engines` set.
Populates the `used_engines` set by checking which engines are used in
`template_url_prepopulate_data.cc`.
"""
print('Populating used engines set')
SE_NAME_REGEX = re.compile(r'.*SearchEngineTier::[A-Za-z]+, &(.+)},')
with open('../search_engines/template_url_prepopulate_data.cc',
'r',
encoding='utf-8') as file:
lines = file.readlines()
for line in lines:
match = SE_NAME_REGEX.match(line)
if match:
used_engines.add(match.group(1))
def delete_files_in_directory(directory_path):
"""Deletes previously generated icons.
Deletes the icons that were previously created and added to directory_path.
Args:
directory_path: The path of the directory where the icons live.
Raises:
OSError: Error occurred while deleting files in {directory_path}
"""
try:
files = os.listdir(directory_path)
for file in files:
file_path = os.path.join(directory_path, file)
# Only remove pngs and don't remove the default icon (globe)
filename = os.path.basename(file_path)
if not filename.endswith('.png') or filename == 'default_favicon.png':
continue
if os.path.isfile(file_path):
os.remove(file_path)
print('All files deleted successfully from ' + directory_path)
except OSError:
print('Error occurred while deleting files in ' + directory_path)
def get_largest_icon_index_and_size(icon_path, name):
"""Fetches the index and size of largest icon in the .ico file.
Some .ico files contain more than 1 icon. The function finds the largest icon
by comparing the icon dimensions and returns its index.
We get the index of the largest icon because scaling an image down is better
than scaling up.
Returns:
A tuple with index of the largest icon in the .ico file and its size.
Args:
icon_path: The path to the .ico file.
name: Name/keyword of the search engine.
"""
images_stream = os.popen('identify ' + icon_path).read()
images_stream_strings = images_stream.splitlines()
max_image_size = 0
max_image_size_index = 0
for index, string in enumerate(images_stream_strings):
# Search for the string dimension example 16x16
image_dimensions = re.search(r'[0-9]+x[0-9]+', string).group()
# The image size is the integer before the 'x' character.
sizes = image_dimensions.split('x')
if sizes[0] != sizes[1]:
print('Warning: Icon for %s is not square' % name)
image_size = int(sizes[0])
if image_size > max_image_size:
max_image_size = image_size
max_image_size_index = index
return (max_image_size_index, max_image_size)
def create_icons_from_json_file():
"""Downloads the icons that are referenced in the json file.
Reads the json file and downloads the icons that are referenced in the
"favicon_url" section of the search_engine.
Scales those icons down to 48x48 size and converts them to PNG format. After
finishing the previous step, the function moves the icons to the destination
directory and runs png optimization.
The function filters the search engines based on the search engines that are
used in `template_url_prepopulate_data.cc` so that icons that are never used
don't get downloaded.
The Google Search icon is not downloaded because it already exists in the
repo.
Raises:
requests.exceptions.RequestException, FileNotFoundError: Error while loading
URL {favicon_url}
"""
print('Creating icons from json file...')
prepopulated_engines_file_path = '../search_engines/prepopulated_engines.json'
image_destination_path = './default_100_percent/search_engine_choice/'
icon_sizes = [48]
favicon_hash_to_icon_name = {}
# Delete the previously added search engine icons
delete_files_in_directory(image_destination_path)
with open(prepopulated_engines_file_path, 'r',
encoding='utf-8') as engines_json:
# Use commentjson to ignore the comments in the json file.
data = commentjson.loads(engines_json.read())
for engine in data['elements']:
# We don't need to download an icon for an engine that's not used.
if engine not in used_engines:
continue
# We don't want to download the google icon because we already have it
# in the internal repo.
if engine == 'google':
continue
favicon_url = data['elements'][engine]['favicon_url']
search_engine_keyword = data['elements'][engine]['keyword']
try:
# Download the icon and rename it as 'original.ico'
img_data = requests.get(
favicon_url,
headers={
# Some search engines 403 even requests for favicons if we don't
# look like a browser
"User-Agent":
("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, "
"like Gecko) Chrome/117.0.0.0 Safari/537.36")
})
icon_name = keyword_to_identifer(search_engine_keyword)
with open('original.ico', 'wb') as original_icon:
original_icon.write(img_data.content)
icon_hash = get_image_hash('original.ico')
# We already have the icon stored in the repo.
if (icon_hash in favicon_hash_to_icon_name):
engine_keyword_to_icon_name[
search_engine_keyword] = favicon_hash_to_icon_name[icon_hash]
os.remove('original.ico')
continue
(largest_index, largest_size) = get_largest_icon_index_and_size(
'original.ico', icon_name)
# Using ImageMagick command line interface, scale the icons, convert
# them to PNG format and move them to their corresponding folders.
last_size = 0
for desired_size in icon_sizes:
if largest_size >= last_size:
last_size = desired_size
desired_size = min(desired_size, largest_size)
os.system('convert original.ico[' + str(largest_index) +
'] -thumbnail ' + str(desired_size) + 'x' +
str(desired_size) + ' ' + icon_name + '.png')
shutil.move(icon_name + '.png', image_destination_path)
engine_keyword_to_icon_name[search_engine_keyword] = icon_name
favicon_hash_to_icon_name[icon_hash] = icon_name
os.remove('original.ico')
# `FileNotFoundError` is thrown if we were not able to download the
# favicon and we try to move it.
except (requests.exceptions.RequestException, FileNotFoundError):
# Favicon URL doesn't load.
print('Error while loading URL ' + favicon_url)
# Engine doesn't have a favicon loaded. We use the
# default icon in that case.
engine_keyword_to_icon_name[search_engine_keyword] = ''
continue
os.system('../../tools/resources/optimize-png-files.sh ' +
image_destination_path)
def generate_icon_resource_code():
"""Links the downloaded icons to their respective resource id.
Generates the code to link the icons to a resource ID in
`search_engine_choice_scaled_resources.grdp`
"""
print('Writing to search_engine_choice_scaled_resources.grdp...')
with open('./search_engine_choice_scaled_resources.grdp',
'w',
encoding='utf-8',
newline='') as grdp_file:
grdp_file.write('<?xml version="1.0" encoding="utf-8"?>\n')
grdp_file.write(
'<!-- This file is generated using generate_search_engine_icons.py'
' -->\n')
grdp_file.write("<!-- Don't modify it manually -->\n")
grdp_file.write('<grit-part>\n')
# Add the google resource id.
grdp_file.write(' <if expr="_google_chrome">\n')
grdp_file.write(' <structure type="chrome_scaled_image"'
' name="IDR_GOOGLE_COM_PNG"'
' file="google_chrome/google_search_logo.png" />\n')
grdp_file.write(' </if>\n')
# Add the remaining resource ids.
for engine_keyword in engine_keyword_to_icon_name:
icon_name = engine_keyword_to_icon_name[engine_keyword]
resource_id = 'IDR_' + keyword_to_identifer(
engine_keyword).upper() + '_PNG'
# No favicon loaded. Use default_favicon.png
if not icon_name:
grdp_file.write(
' <structure type="chrome_scaled_image" name="' + resource_id +
'" file="search_engine_choice/default_favicon.png" />\n')
else:
grdp_file.write(' <structure type="chrome_scaled_image" name="' +
resource_id + '" file="search_engine_choice/' +
icon_name + '.png" />\n')
grdp_file.write('</grit-part>\n')
def create_adding_icons_to_source_function():
"""Generates the `AddGeneratedIconResources` in
`search_engine_choice/generated_icon_utils.cc`.
Generates the function that will be used to populate the `WebUIDataSource`
with the generated icons.
"""
print('Creating `AddGeneratedIconResources` function...')
with open(
'../../chrome/browser/ui/webui/search_engine_choice/generated_icon_utils.cc',
'w',
encoding='utf-8',
newline='') as utils_file:
# Add the copyright notice.
utils_file.write('// Copyright 2023 The Chromium Authors\n')
utils_file.write('// Use of this source code is governed by a BSD-style'
' license that can be\n')
utils_file.write('// found in the LICENSE file.\n\n')
# Include the required header files.
utils_file.write(
'#include "chrome/browser/ui/webui/search_engine_choice/icon_utils.h"\n\n'
)
utils_file.write('#include "base/check_op.h"\n')
utils_file.write('#include "build/branding_buildflags.h"\n')
utils_file.write(
'#include "components/grit/components_scaled_resources.h"\n')
utils_file.write(
'#include "content/public/browser/web_ui_data_source.h"\n\n')
# Create the function name.
utils_file.write(
'// This code is generated using `generate_search_engine_icons.py`.'
" Don't modify it manually.\n")
utils_file.write('void AddGeneratedIconResources(content::WebUIDataSource*'
' source, const std::string& directory) {\n')
utils_file.write('\tCHECK(source);\n')
utils_file.write("\tCHECK_EQ(directory.back(), '/');\n")
# Add google to the source
utils_file.write('\t#if BUILDFLAG(GOOGLE_CHROME_BRANDING)\n')
utils_file.write('\tsource->AddResourcePath(directory + "google_com.png",'
' IDR_GOOGLE_COM_PNG);\n')
utils_file.write('\t#endif\n')
for engine_keyword in engine_keyword_to_icon_name:
engine_name = keyword_to_identifer(engine_keyword)
local_image_path = engine_name + '.png'
image_resource_id = 'IDR_' + engine_name.upper() + '_PNG'
utils_file.write('\tsource->AddResourcePath(directory + "' +
local_image_path + '", ' + image_resource_id + ');\n')
utils_file.write('}\n')
if sys.platform != 'linux':
print(
'Warning: This script has not been tested outside of the Linux platform')
# Move to working directory to `src/components/resources/`.
current_file_path = os.path.dirname(__file__)
os.chdir(current_file_path)
os.chdir('../../components/resources')
# A set of search engines that are used in `template_url_prepopulate_data.cc`
used_engines = set()
# This is a dictionary of engine keyword to corresponding icon name. Have an
# empty icon name would mean that we weren't able to download the favicon for
# that engine. We use the default favicon in that case.
engine_keyword_to_icon_name = {}
populate_used_engines()
create_icons_from_json_file()
generate_icon_resource_code()
create_adding_icons_to_source_function()
# Format the generated code
os.system('git cl format')
print('Icon and code generation completed.')
|