chromium/tools/search_engine_choice/download_search_engine_icons.py

# 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.
"""Handles the download of the search engine favicons.

For all search engines referenced in search_engine_countries-inc.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 search_engine_countries-inc.cc changes the list of
search engines used per country, or whenever prepopulated_engines.json changes
a favicon.

To run:
`python3 tools/search_engine_choice/download_search_engine_icons.py`.
"""

import hashlib
import json
import os
import sys
import requests

import search_engine_icons_utils


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 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.
      filename = os.path.basename(file_path)
      if filename.endswith('.png') and 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 download_icons_from_android_search():
  """Downloads icons from the android_search gstatic directory.

  Goes through all search engines in `prepopulated_engines.json` and downloads
  the corresponding 96x96 icon from the appropriate subfolder of
  https://www.gstatic.com/android_search/search_providers/. Because there is no
  way to list the contents of a directory on gstatic and because some
  subfolders are not named exactly the same as in `prepopulated_engines.json`,
  this function loads a config file `generate_search_engine_icons_config.json`
  with the extra information needed to locate the icons.

  The Google Search icon is not downloaded because it already exists in the
  repo. Search engines not relevant to the to the default search engine choice
  screen are ignored.
  """
  for percent in ['100', '200', '300']:
    delete_files_in_directory(
        f'components/resources/default_{percent}_percent/search_engine_choice')

  with open(search_engine_icons_utils.config_file_path, 'r',
            encoding='utf-8') as config_json:
    config_data = json.loads(json_comment_eater.Nom(config_json.read()))
    icon_hash_to_name = {}

    for (engine, keyword) in sorted(
        search_engine_icons_utils.get_used_engines_with_keywords("")):
      icon_name = search_engine_icons_utils.keyword_to_identifer(keyword)
      icon_full_path = f'components/resources/default_100_percent/search_engine_choice/{icon_name}.png'
      if engine in config_data['engine_aliases']:
        engine = config_data['engine_aliases'][engine]

      directory_url = f'https://www.gstatic.com/android_search/search_providers/{engine}/'
      try_filenames = []
      if engine in config_data['non_default_icon_filenames']:
        try_filenames = [
            config_data['non_default_icon_filenames'][engine] + 'mdpi.png'
        ]
      try_filenames = try_filenames + [
          f'{engine}_icon_mdpi.png',
          f'{engine}_mdpi.png',
          'mdpi.png',
      ]
      any_found = False
      for filename in try_filenames:
        icon_url = directory_url + filename
        try:
          img_data = requests.get(icon_url)
        except requests.exceptions.RequestException as e:
          print('Error when loading URL {icon_url}: {e}')
          continue
        if img_data.status_code == 200:
          with open(icon_full_path, 'wb') as icon_file:
            icon_file.write(img_data.content)
          any_found = True
          break
      if not any_found:
        print('WARNING: no icon found for search engine: ' + engine)
        continue

      icon_hash = get_image_hash(icon_full_path)
      if icon_hash in icon_hash_to_name:
        # We already have this icon.
        engine_keyword_to_icon_name[keyword] = icon_hash_to_name[icon_hash]
        os.remove(icon_full_path)
        continue

      # Download hidpi versions
      # If the low dpi version is basename_mdpi.png, download basename_xhdpi.png
      # and basename_xxhdpi.png.
      for (resource_path, hidpi) in [('default_200_percent', 'xhdpi'),
                                     ('default_300_percent', 'xxhdpi')]:
        # Replace the substring "mdpi" by "xhdpi" or "xxhdpi" from the end.
        (basename, mdpi_suffix, png_extension) = icon_url.rpartition('mdpi')
        hidpi_url = basename + hidpi + png_extension
        hidpi_path = f'components/resources/{resource_path}/search_engine_choice/{icon_name}.png'
        try:
          img_data = requests.get(hidpi_url)
        except requests.exceptions.RequestException as e:
          print('Error when loading URL {hidpi_url}: {e}')
          continue
        if img_data.status_code == 200:
          with open(hidpi_path, 'wb') as icon_file:
            icon_file.write(img_data.content)
        else:
          print('WARNING: no %s icon found for search engine: %s' %
                (hidpi, engine))

      engine_keyword_to_icon_name[keyword] = icon_name
      icon_hash_to_name[icon_hash] = icon_name
  print('Finished downloading icons')
  os.system('tools/resources/optimize-png-files.sh search_engine_choice')


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('components/resources/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, sorted alphabetically.
    resources = []
    for engine_keyword, icon_name in engine_keyword_to_icon_name.items():
      resource_id = search_engine_icons_utils.keyword_to_resource_name(
          engine_keyword)
      resources.append((resource_id, icon_name))

    for resource_id, icon_name in sorted(resources):
      grdp_file.write(
          f'  <structure type="chrome_scaled_image" name="{resource_id}" file="search_engine_choice/{icon_name}.png" />\n'
      )

    grdp_file.write('</grit-part>\n')


if sys.platform != 'linux':
  print(
      'Warning: This script has not been tested outside of the Linux platform')

# Move to working directory to `src/`.
current_file_path = os.path.dirname(__file__)
os.chdir(current_file_path)
os.chdir('../../')

sys.path.insert(0,
                os.path.normpath(current_file_path + "/../json_comment_eater"))
try:
  import json_comment_eater
finally:
  sys.path.pop(0)

# 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.
engine_keyword_to_icon_name = {}

download_icons_from_android_search()
generate_icon_resource_code()
print('Icon download completed.')