chromium/components/vector_icons/aggregate_vector_icons.py

# Copyright 2015 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import fileinput
import glob
import optparse
import os
import re
import shlex
import sys
import textwrap

CANVAS_DIMENSIONS = "CANVAS_DIMENSIONS"
CPP_COMMENT_DELIMITER = "//"
REFERENCE_SIZE_DIP = 48
TEMPLATE_PLACEHOLDER = "TEMPLATE_PLACEHOLDER"


def Error(msg):
  print(msg, file=sys.stderr)
  sys.exit(1)


def CamelCase(name, suffix):
  words = name.split("_")
  words = [w.capitalize() for w in words]
  return "k" + "".join(words) + suffix


def GetPathName(name, size=None):
  return CamelCase(name, "{}Path".format(size) if size != None else "Path")


def GetRepListName(name):
  return CamelCase(name, "RepList")


def GetIconName(name):
  return CamelCase(name, "Icon")


def AddIconToDictionary(icon_file, new_icon, icon_size, icon_index):
  if icon_size in icon_index:
    Error("Duplicate icon of size {} found in {}.".format(icon_size, icon_file))
  icon_index[icon_size] = "\n".join(new_icon)
  return icon_index


def ExtractIconReps(icon_file_name):
  """Reads the contents of the given icon file and returns a dictionary of icon
     sizes to vector commands for different icon representations stored in that
     file.

  Args:
      icon_file_name: The file path of the icon file to read.
  """
  with open(icon_file_name, "r") as icon_file:
    icon_file_contents = icon_file.readlines()

  current_icon_size = REFERENCE_SIZE_DIP
  icon_sizes = []
  current_icon_representation = []
  icon_representations = {}
  for line in icon_file_contents:
    # Strip comments and empty lines.
    line = line.partition(CPP_COMMENT_DELIMITER)[0].strip()
    if not line:
      continue
    # Retrieve sizes specified by CANVAS_DIMENSIONS to ensure icons are added in
    # sorted order by size descending.
    if line.startswith(CANVAS_DIMENSIONS):
      sizes = re.findall(r"\d+", line)
      if len(sizes) != 1:
        Error("Malformed {} line in {} - it should specify exactly one size."
              .format(CANVAS_DIMENSIONS, icon_file_name))
      icon_sizes.append(int(sizes[0]))

      # All icons except the first / default icon must start with
      # "CANVAS_DIMENSIONS", so rely on it here as a icon delimiter.
      if current_icon_representation:
        icon_representations = AddIconToDictionary(
            icon_file_name, current_icon_representation, current_icon_size,
            icon_representations)
        current_icon_representation = []
      current_icon_size = icon_sizes[-1]

    current_icon_representation.append(line)
  if current_icon_representation:
    icon_representations = AddIconToDictionary(
        icon_file_name, current_icon_representation, current_icon_size,
        icon_representations)

  if not icon_representations:
    Error("Didn't find any icons in {}.".format(icon_file_name))

  if len(icon_representations) != len(icon_sizes):
    icon_sizes.insert(0, REFERENCE_SIZE_DIP)
  if sorted(icon_sizes, reverse=True) != icon_sizes:
    Error("The icons in {} should be sorted in descending order of size."
          .format(icon_file_name))
  return icon_representations


def AggregateVectorIcons(working_directory, file_list, output_cc, output_h):
  """Compiles all .icon files in a directory into two C++ files.

  Args:
      working_directory: The path to the directory that holds the .icon files
          and C++ templates.
      file_list: A file containing the list of vector icon files to process.
      output_cc: The path that should be used to write the .cc file.
      output_h: The path that should be used to write the .h file.
  """

  # For each file in |file_list|, place it in |path_map| if its extension is
  # .icon. This will map the icon's name to its path, e.g.,
  # path_map['cat'] = 'foo/bar/cat.icon'.
  icon_list = []
  with open(file_list, "r") as f:
    file_list_contents = f.read()
  icon_list = shlex.split(file_list_contents)

  path_map = {}

  for icon_path in icon_list:
    (icon_name, extension) = os.path.splitext(os.path.basename(icon_path))

    if extension != ".icon":
      Error("Only filenames " + icon_name + ".icon are allowed.")

    if icon_name not in path_map:
      path_map[icon_name] = icon_path
    else:
      Error("A vector icon with name '" + icon_name + "' already exists.")

  # Generate the file vector_icons.h which declares a variable for each
  # icon in |path_map|. The variable name is derived from the icon name by
  # converting to camel case, prepending 'k', and appending 'Icon'. For
  # example, the icon 'foo_bar' will have the variable name kFooBarIcon.
  with open(os.path.join(working_directory, "vector_icons.h.template"),
            "r") as input_header_template:
    header_template_contents = input_header_template.readlines()

  output_header = open(output_h, "w")
  for line in header_template_contents:
    if not TEMPLATE_PLACEHOLDER in line:
      output_header.write(line)
      continue

    for icon in path_map:
      (icon_name, extension) = os.path.splitext(
                               os.path.basename(path_map[icon]))
      output_header.write("VECTOR_ICON_TEMPLATE_H({})\n".format(
          GetIconName(icon_name)))
  output_header.close()

  # Copy the vector icon drawing commands from the .icon files, splitting them
  # into their individual icon representations, and use them to generate
  # vector_icons.cc, which defines the variables declared in vector_icons.h.
  input_cc_template = open(
      os.path.join(working_directory, "vector_icons.cc.template"))
  cc_template_contents = input_cc_template.readlines()
  input_cc_template.close()

  output_cc = open(output_cc, "w")
  for line in cc_template_contents:
    if not TEMPLATE_PLACEHOLDER in line:
      output_cc.write(line)
      continue
    for icon in path_map:
      (icon_name, extension) = os.path.splitext(
                               os.path.basename(path_map[icon]))

      icon_representations = ExtractIconReps(path_map[icon])
      icon_representation_strings = []
      for i, size in enumerate(sorted(icon_representations, reverse=True)):
        vector_commands = icon_representations[size]
        icon_path_name = GetPathName(icon_name, size if i != 0 else None)
        # Store the vector-drawing commands for foo_bar.icon in the temporary
        # variable kFooBarPath.
        output_cc.write("VECTOR_ICON_REP_TEMPLATE({}, {})\n".format(
            icon_path_name, vector_commands))
        icon_representation_strings.append(
            "{{{0}, std::size({0})}}".format(icon_path_name))

      # Another temporary variable kFooBarRepList is used to create all the
      # VectorIconReps inline, with a pointer to it in the final VectorIcon.
      output_cc.write("VECTOR_ICON_TEMPLATE_CC({}, {}, {})\n".format(
          GetRepListName(icon_name), GetIconName(icon_name),
          ", ".join(icon_representation_strings)))

  output_cc.close()


def main():
  parser = optparse.OptionParser()
  parser.add_option("--working_directory",
                    help="The directory to look for template C++ as well as "
                         "icon files.")
  parser.add_option("--file_list",
                    help="A response file containing the list of icon files "
                         "to be processed.")
  parser.add_option("--output_cc",
                    help="The path to output the CC file to.")
  parser.add_option("--output_h",
                    help="The path to output the header file to.")

  (options, args) = parser.parse_args()

  AggregateVectorIcons(options.working_directory,
                       options.file_list,
                       options.output_cc,
                       options.output_h)


if __name__ == "__main__":
  main()