| #!/usr/bin/python |
| # Copyright 2018 The Cobalt Authors. All Rights Reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Filters fonts.xml to include only desired font packages. |
| |
| This is meant to be used in a GYP file to generate the list of fonts as the |
| input files for a target, as well as to re-write fonts.xml to include only |
| desired fonts. |
| """ |
| |
| import argparse |
| from functools import partial |
| import sys |
| from xml.dom import Node |
| from xml.dom.minidom import parse as parse_xml |
| |
| NORMAL_WEIGHT = 400 |
| BOLD_WEIGHT = 700 |
| |
| NORMAL_STYLE = 'normal' |
| ITALIC_STYLE = 'italic' |
| |
| |
| def SelectFont(font_node, package_categories): |
| """Filter function that keeps fonts according to the package font category. |
| |
| Args: |
| font_node: The <font> node to evaluate. |
| package_categories: Dictionary of package name to category number. |
| The category values are described in cobalt/content/fonts/README.md |
| Returns: |
| True to keep this font, False to delete it. |
| """ |
| weight = font_node.getAttribute('weight') |
| if not weight: |
| weight = NORMAL_WEIGHT |
| else: |
| weight = int(weight) |
| |
| style = font_node.getAttribute('style') |
| if not style: |
| style = NORMAL_STYLE |
| |
| package_name = GetContainerName('package', font_node) |
| |
| # Keep all fonts not in a named package. |
| if not package_name: |
| return True |
| |
| # When a package is excluded, keep fonts in the '!' version of the package. |
| excluded = False |
| if package_name[0] == '!': |
| package_name = package_name[1:] |
| excluded = True |
| |
| category = package_categories.get(package_name, '0') |
| |
| if excluded: |
| return category == '0' |
| elif category == '0': |
| return False |
| elif category == '1': |
| return weight == NORMAL_WEIGHT and style == NORMAL_STYLE |
| elif category == '2': |
| return weight in (NORMAL_WEIGHT, BOLD_WEIGHT) and style == NORMAL_STYLE |
| elif category == '3': |
| return (weight in (NORMAL_WEIGHT, BOLD_WEIGHT) and |
| style in (NORMAL_STYLE, ITALIC_STYLE)) |
| elif category == '4': |
| return True |
| else: |
| raise ValueError( |
| 'Package category for "%s" must be between 0 and 4 (is: %s)' % |
| (package_name, category)) |
| |
| |
| def FilterFonts(filter_function, node, families, fonts): |
| """Filter fonts in the document parsed from fonts.xml. |
| |
| Recursively traverses the document tree in a depth-first order, deleting |
| unwanted nodes after visiting them. |
| |
| Args: |
| filter_function: predicate method called for each font node. |
| node: the root of the tree to evaluate recursively. |
| families: list collected of remaining font families. |
| fonts: list collected of remaining font files. |
| Returns: |
| True to keep this node, False to delete it. |
| """ |
| # Depth-first traversal, deleting children that want to be deleted before |
| # processing their parents. Make a new list of children so we can delete some |
| # while iterating. |
| for child in list(node.childNodes): |
| if not FilterFonts(filter_function, child, families, fonts): |
| node.removeChild(child).unlink() |
| |
| if node.nodeType == Node.TEXT_NODE: |
| # Only keep text that has more than just whitespace. |
| return node.data.strip() |
| elif node.nodeType != Node.ELEMENT_NODE: |
| # Only keep text and elements (no comments). |
| return False |
| |
| # Only keep alias elements that refer to a family that was kept. |
| if node.nodeName == 'alias': |
| return node.getAttribute('to') in families |
| |
| # Delete nodes where all children were deleted. |
| # We already kept relevant text and aliases, which have no children. |
| if not node.childNodes: |
| return False |
| |
| if node.nodeName == 'package': |
| # Elevate all package children to here, and delete the package node. |
| for child in list(node.childNodes): |
| node.parentNode.insertBefore(node.removeChild(child), node) |
| return False |
| elif node.nodeName == 'family': |
| # Remember the name of kept families (i.e. still have font children). |
| families.append(node.getAttribute('name')) |
| elif node.nodeName == 'font': |
| # Only keep fonts that pass the filter_function. |
| if filter_function(node): |
| fonts.append(node.firstChild.data) |
| return True |
| else: |
| return False |
| |
| return True |
| |
| |
| def GetContainerName(container_type, node): |
| if node is None: |
| return None |
| elif node.nodeName == container_type: |
| return node.getAttribute('name') |
| else: |
| return GetContainerName(container_type, node.parentNode) |
| |
| |
| def DoMain(argv): |
| """Main function called from the command line. |
| |
| Args: |
| argv: Command line parameters, without the program name. |
| Returns: |
| If --fonts_dir is specified, a newline-separated list of font files. |
| """ |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '-i', |
| '--input_xml', |
| required=True, |
| help='path to fonts.xml to be filtered') |
| parser.add_argument( |
| '-o', '--output_xml', help='path to write a filtered XML file') |
| parser.add_argument( |
| '-f', |
| '--fonts_dir', |
| help='prints a list of font files prefixed by the ' |
| 'specified FONTS_DIR (for GYP inputs)') |
| parser.add_argument('package_categories', nargs=argparse.REMAINDER) |
| options = parser.parse_args(argv) |
| |
| if not (options.output_xml or options.fonts_dir): |
| parser.error('No output, specify output_xml, fonts_dir, or both') |
| |
| # Make a dictionary mapping package name to category. |
| # E.g. ['sans-serif=1', 'serif=2'] becomes {'sans-serif':'1', 'serif':'2'} |
| package_categories = dict( |
| pkg.split('=') for pkg in options.package_categories) |
| |
| fonts_doc = parse_xml(options.input_xml) |
| |
| kept_families = [] |
| kept_fonts = [] |
| filter_function = partial(SelectFont, package_categories=package_categories) |
| FilterFonts(filter_function, fonts_doc, kept_families, kept_fonts) |
| |
| if options.output_xml: |
| with open(options.output_xml, 'w') as f: |
| f.write(fonts_doc.toprettyxml(indent=' ')) |
| |
| if options.fonts_dir: |
| # Join with '/' rather than os.path.join() because this is for GYP, which |
| # even on Windows wants slashes rather than backslashes. |
| # Make a set for unique fonts since .ttc files may be listed more than once. |
| result = [ |
| '/'.join((options.fonts_dir, font)) for font in sorted(set(kept_fonts)) |
| ] |
| return '\n'.join(result) |
| |
| |
| def main(argv): |
| result = DoMain(argv[1:]) |
| if result: |
| print(result) |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv)) |