File: generate_search_engine_icons.py

package info (click to toggle)
chromium 120.0.6099.224-1~deb11u1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 6,112,112 kB
  • sloc: cpp: 32,907,025; ansic: 8,148,123; javascript: 3,679,536; python: 2,031,248; asm: 959,718; java: 804,675; xml: 617,256; sh: 111,417; objc: 100,835; perl: 88,443; cs: 53,032; makefile: 29,579; fortran: 24,137; php: 21,162; tcl: 21,147; sql: 20,809; ruby: 17,735; pascal: 12,864; yacc: 8,045; lisp: 3,388; lex: 1,323; ada: 727; awk: 329; jsp: 267; csh: 117; exp: 43; sed: 37
file content (363 lines) | stat: -rw-r--r-- 13,545 bytes parent folder | download
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.')