From 6e4ed7c17460a71b5a9f6075eb62712c754b369b Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 30 Mar 2022 15:39:11 +0100 Subject: [PATCH] Remove font "fixing" scripts --- nototools/android_patches.py | 469 -------------------- nototools/autofix_for_phase3.py | 544 ------------------------ nototools/autofix_for_release.py | 398 ----------------- nototools/fix_khmer_and_lao_coverage.py | 90 ---- nototools/fix_noto_cjk_thin.py | 88 ---- nototools/swat_license.py | 483 --------------------- 6 files changed, 2072 deletions(-) delete mode 100755 nototools/android_patches.py delete mode 100755 nototools/autofix_for_phase3.py delete mode 100755 nototools/autofix_for_release.py delete mode 100755 nototools/fix_khmer_and_lao_coverage.py delete mode 100755 nototools/fix_noto_cjk_thin.py delete mode 100755 nototools/swat_license.py diff --git a/nototools/android_patches.py b/nototools/android_patches.py deleted file mode 100755 index 29a5530f..00000000 --- a/nototools/android_patches.py +++ /dev/null @@ -1,469 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2016 Google Inc. 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. - -"""Patches for Android versions of Noto fonts.""" - -import argparse -import codecs -import glob -import os -from os import path -import shutil -import tempfile - -from nototools.py23 import unichr -from nototools import subset -from nototools import coverage -from nototools import fix_khmer_and_lao_coverage as merger -from nototools import font_data -from nototools import tool_utils -from nototools import ttc_utils -from nototools import unicode_data - -from fontTools import ttLib - - -def patch_hyphen(srcdir, dstdir, copy_unchanged=True): - """Add hyphen-minus glyphs to fonts that need it. - - This is to enable languages to be hyphenated properly, - since Minikin's itemizer currently shows tofus if an - automatically hyphenated word is displated in a font - that has neither HYPHEN nor HYPHEN-MINUS. - - The list of font names comes from LANG_TO_SCRIPT in - tools/font/fontchain_lint.py. - - (In practice only U+002D HYPHEN-MINUS is added, since Noto LGC fonts - don't have U+2010 HYPHEN.) - - Bug: 21570828""" - - # Names of fonts for which Android requires a hyphen. - # This list omits Japanese and Korean. - script_names = [ - "Armenian", - "Ethiopic", - "Bengali", - "Gujarati", - "Devanagari", - "Kannada", - "Malayalam", - "Oriya", - "Gurmukhi", - "Tamil", - "Telugu", - ] - - HYPHENS = {0x002D, 0x2010} - - for sn in script_names: - globexp = path.join(srcdir, "Noto*%s-*.ttf" % sn) - fonts = glob.glob(globexp) - if not fonts: - continue - fonts = [path.basename(f) for f in fonts] - for font_name in fonts: - lgc_font_name = font_name.replace(sn, "") - - font_file = path.join(srcdir, font_name) - lgc_font_file = path.join(srcdir, lgc_font_name) - - chars_to_add = ( - HYPHENS - coverage.character_set(font_file) - ) & coverage.character_set(lgc_font_file) - - if chars_to_add: - print("patch hyphens", font_name) - merger.merge_chars_from_bank( - path.join(srcdir, font_name), - path.join(srcdir, lgc_font_name), - path.join(srcdir, font_name), - chars_to_add, - ) - else: - if copy_unchanged: - shutil.copy2( - path.join(srcdir, font_name), path.join(dstdir, font_name) - ) - print("%s already has hyphens, copying" % font_name) - else: - print("%s already has hyphens" % font_name) - - -def _remove_cjk_emoji(cjk_font_names, srcdir, dstdir): - """ - Remove default emoji characters from CJK fonts. - - Twenty-six characters that Unicode Technical Report #51 "Unicode - Emoji" defines as defaulting to emoji styles used to be displayed as - black and white ("text" style) before this. This patch removes those - characters from Noto CJK fonts, so they get displayed as color. - - (1c4749e20391a4) - """ - - # Since subsetting changes tables in a way that would prevent a compact - # .ttc file, this simply removes entries from the cmap table. This - # does not affect other tables in the font. There are no emoji presentation - # variation sequences in the fonts. - - def _remove_from_cmap(infile, outfile, exclude=[]): - font = ttLib.TTFont(infile) - font_data.delete_from_cmap(font, exclude) - font.save(outfile) - - # Characters supported in Noto CJK fonts that UTR #51 recommends default to - # emoji-style. - EMOJI_IN_CJK = { - 0x26BD, # โšฝ SOCCER BALL - 0x26BE, # โšพ BASEBALL - 0x1F18E, # ๐Ÿ†Ž NEGATIVE SQUARED AB - 0x1F191, # ๐Ÿ†‘ SQUARED CL - 0x1F192, # ๐Ÿ†’ SQUARED COOL - 0x1F193, # ๐Ÿ†“ SQUARED FREE - 0x1F194, # ๐Ÿ†” SQUARED ID - 0x1F195, # ๐Ÿ†• SQUARED NEW - 0x1F196, # ๐Ÿ†– SQUARED NG - 0x1F197, # ๐Ÿ†— SQUARED OK - 0x1F198, # ๐Ÿ†˜ SQUARED SOS - 0x1F199, # ๐Ÿ†™ SQUARED UP WITH EXCLAMATION MARK - 0x1F19A, # ๐Ÿ†š SQUARED VS - 0x1F201, # ๐Ÿˆ SQUARED KATAKANA KOKO - 0x1F21A, # ๐Ÿˆš SQUARED CJK UNIFIED IDEOGRAPH-7121 - 0x1F22F, # ๐Ÿˆฏ SQUARED CJK UNIFIED IDEOGRAPH-6307 - 0x1F232, # ๐Ÿˆฒ SQUARED CJK UNIFIED IDEOGRAPH-7981 - 0x1F233, # ๐Ÿˆณ SQUARED CJK UNIFIED IDEOGRAPH-7A7A - 0x1F234, # ๐Ÿˆด SQUARED CJK UNIFIED IDEOGRAPH-5408 - 0x1F235, # ๐Ÿˆต SQUARED CJK UNIFIED IDEOGRAPH-6E80 - 0x1F236, # ๐Ÿˆถ SQUARED CJK UNIFIED IDEOGRAPH-6709 - 0x1F238, # ๐Ÿˆธ SQUARED CJK UNIFIED IDEOGRAPH-7533 - 0x1F239, # ๐Ÿˆน SQUARED CJK UNIFIED IDEOGRAPH-5272 - 0x1F23A, # ๐Ÿˆบ SQUARED CJK UNIFIED IDEOGRAPH-55B6 - 0x1F250, # ๐Ÿ‰ CIRCLED IDEOGRAPH ADVANTAGE - 0x1F251, # ๐Ÿ‰‘ CIRCLED IDEOGRAPH ACCEPT - } - - # Characters we have decided we are doing as emoji-style in Android, - # despite UTR #51's recommendation - ANDROID_EMOJI = { - 0x2600, # โ˜€ BLACK SUN WITH RAYS - 0x2601, # โ˜ CLOUD - 0x260E, # โ˜Ž BLACK TELEPHONE - 0x261D, # โ˜ WHITE UP POINTING INDEX - 0x263A, # โ˜บ WHITE SMILING FACE - 0x2660, # โ™  BLACK SPADE SUIT - 0x2663, # โ™ฃ BLACK CLUB SUIT - 0x2665, # โ™ฅ BLACK HEART SUIT - 0x2666, # โ™ฆ BLACK DIAMOND SUIT - 0x270C, # โœŒ VICTORY HAND - 0x2744, # โ„ SNOWFLAKE - 0x2764, # โค HEAVY BLACK HEART - } - - # We don't want support for ASCII control chars. - CONTROL_CHARS = tool_utils.parse_int_ranges("0000-001F") - - EXCLUDED_CODEPOINTS = sorted(EMOJI_IN_CJK | ANDROID_EMOJI | CONTROL_CHARS) - - for font_name in cjk_font_names: - print("remove cjk emoji", font_name) - _remove_from_cmap( - path.join(srcdir, font_name), - path.join(dstdir, font_name), - exclude=EXCLUDED_CODEPOINTS, - ) - - -def patch_cjk_ttc(ttc_srcfile, ttc_dstfile): - """Take the source ttc, break it apart, remove the cjk emoji - from each file, then repackage them into a new ttc.""" - - tmp_dir = tempfile.mkdtemp() - font_names = ttc_utils.ttcfile_extract(ttc_srcfile, tmp_dir) - tmp_patched_dir = path.join(tmp_dir, "patched") - os.mkdir(tmp_patched_dir) - _remove_cjk_emoji(font_names, tmp_dir, tmp_patched_dir) - # have ttcfile_build resolve names relative to patched dir - with tool_utils.temp_chdir(tmp_patched_dir): - ttc_utils.ttcfile_build(ttc_dstfile, font_names) - shutil.rmtree(tmp_dir) - - -def patch_cjk_ttcs(srcdir, dstdir): - """Call patch_cjk_ttc for each ttc file in srcdir, writing the - result to dstdir using the same name.""" - - if not path.isdir(srcdir): - print("%s is not a directory" % srcdir) - return - - ttc_files = [f for f in os.listdir(srcdir) if f.endswith(".ttc")] - if not ttc_files: - print("no .ttc file to patch in %s" % srcdir) - return - - tool_utils.ensure_dir_exists(dstdir) - for f in ttc_files: - patch_cjk_ttc(path.join(srcdir, f), path.join(dstdir, f)) - - -# below are used by _subset_symbols - -# Unicode blocks that we want to include in the font -BLOCKS_TO_INCLUDE = """ -20D0..20FF; Combining Diacritical Marks for Symbols -2100..214F; Letterlike Symbols -2190..21FF; Arrows -2200..22FF; Mathematical Operators -2300..23FF; Miscellaneous Technical -2400..243F; Control Pictures -2440..245F; Optical Character Recognition -2460..24FF; Enclosed Alphanumerics -2500..257F; Box Drawing -2580..259F; Block Elements -25A0..25FF; Geometric Shapes -2600..26FF; Miscellaneous Symbols -2700..27BF; Dingbats -27C0..27EF; Miscellaneous Mathematical Symbols-A -27F0..27FF; Supplemental Arrows-A -2800..28FF; Braille Patterns -2A00..2AFF; Supplemental Mathematical Operators -""" - -# One-off characters to be included, needed for backward compatibility and -# supporting various character sets, including ARIB sets and black and white -# emoji -ONE_OFF_ADDITIONS = { - 0x27D0, # โŸ WHITE DIAMOND WITH CENTRED DOT - 0x2934, # โคด ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS - 0x2935, # โคต ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS - 0x2985, # โฆ… LEFT WHITE PARENTHESIS - 0x2986, # โฆ† RIGHT WHITE PARENTHESIS - 0x2B05, # โฌ… LEFTWARDS BLACK ARROW - 0x2B06, # โฌ† UPWARDS BLACK ARROW - 0x2B07, # โฌ‡ DOWNWARDS BLACK ARROW - 0x2B24, # โฌค BLACK LARGE CIRCLE - 0x2B2E, # โฌฎ BLACK VERTICAL ELLIPSE - 0x2B2F, # โฌฏ WHITE VERTICAL ELLIPSE - 0x2B56, # โญ– HEAVY OVAL WITH OVAL INSIDE - 0x2B57, # โญ— HEAVY CIRCLE WITH CIRCLE INSIDE - 0x2B58, # โญ˜ HEAVY CIRCLE - 0x2B59, # โญ™ HEAVY CIRCLED SALTIRE - 0x1F19B, # ๐Ÿ†› SQUARED THREE D - 0x1F19C, # ๐Ÿ†œ SQUARED SECOND SCREEN - 0x1F19D, # ๐Ÿ† SQUARED TWO K;So;0;L;;;;;N;;;;; - 0x1F19E, # ๐Ÿ†ž SQUARED FOUR K;So;0;L;;;;;N;;;;; - 0x1F19F, # ๐Ÿ†Ÿ SQUARED EIGHT K;So;0;L;;;;;N;;;;; - 0x1F1A0, # ๐Ÿ†  SQUARED FIVE POINT ONE;So;0;L;;;;;N;;;;; - 0x1F1A1, # ๐Ÿ†ก SQUARED SEVEN POINT ONE;So;0;L;;;;;N;;;;; - 0x1F1A2, # ๐Ÿ†ข SQUARED TWENTY-TWO POINT TWO;So;0;L;;;;;N;;;;; - 0x1F1A3, # ๐Ÿ†ฃ SQUARED SIXTY P;So;0;L;;;;;N;;;;; - 0x1F1A4, # ๐Ÿ†ค SQUARED ONE HUNDRED TWENTY P;So;0;L;;;;;N;;;;; - 0x1F1A5, # ๐Ÿ†ฅ SQUARED LATIN SMALL LETTER D;So;0;L;;;;;N;;;;; - 0x1F1A6, # ๐Ÿ†ฆ SQUARED HC;So;0;L;;;;;N;;;;; - 0x1F1A7, # ๐Ÿ†ง SQUARED HDR;So;0;L;;;;;N;;;;; - 0x1F1A8, # ๐Ÿ†จ SQUARED HI-RES;So;0;L;;;;;N;;;;; - 0x1F1A9, # ๐Ÿ†ฉ SQUARED LOSSLESS;So;0;L;;;;;N;;;;; - 0x1F1AA, # ๐Ÿ†ช SQUARED SHV;So;0;L;;;;;N;;;;; - 0x1F1AB, # ๐Ÿ†ซ SQUARED UHD;So;0;L;;;;;N;;;;; - 0x1F1AC, # ๐Ÿ†ฌ SQUARED VOD;So;0;L;;;;;N;;;;; - 0x1F23B, # ๐Ÿˆป SQUARED CJK UNIFIED IDEOGRAPH-914D -} - -# letter-based characters, provided by Roboto -# TODO see if we need to change this subset based on Noto Serif coverage -# (so the serif fallback chain would support them) -LETTERLIKE_CHARS_IN_ROBOTO = { - 0x2100, # โ„€ ACCOUNT OF - 0x2101, # โ„ ADDRESSED TO THE SUBJECT - 0x2103, # โ„ƒ DEGREE CELSIUS - 0x2105, # โ„… CARE OF - 0x2106, # โ„† CADA UNA - 0x2109, # โ„‰ DEGREE FAHRENHEIT - 0x2113, # โ„“ SCRIPT SMALL L - 0x2116, # โ„– NUMERO SIGN - 0x2117, # โ„— SOUND RECORDING COPYRIGHT - 0x211E, # โ„ž PRESCRIPTION TAKE - 0x211F, # โ„Ÿ RESPONSE - 0x2120, # โ„  SERVICE MARK - 0x2121, # โ„ก TELEPHONE SIGN - 0x2122, # โ„ข TRADE MARK SIGN - 0x2123, # โ„ฃ VERSICLE - 0x2125, # โ„ฅ OUNCE SIGN - 0x2126, # โ„ฆ OHM SIGN - 0x212A, # โ„ช KELVIN SIGN - 0x212B, # โ„ซ ANGSTROM SIGN - 0x212E, # โ„ฎ ESTIMATED SYMBOL - 0x2132, # โ„ฒ TURNED CAPITAL F - 0x213B, # โ„ป FACSIMILE SIGN - 0x214D, # โ… AKTIESELSKAB - 0x214F, # โ… SYMBOL FOR SAMARITAN SOURCE -} - -ANDROID_EMOJI = { - 0x2600, # โ˜€ BLACK SUN WITH RAYS - 0x2601, # โ˜ CLOUD - 0x260E, # โ˜Ž BLACK TELEPHONE - 0x261D, # โ˜ WHITE UP POINTING INDEX - 0x263A, # โ˜บ WHITE SMILING FACE - 0x2660, # โ™  BLACK SPADE SUIT - 0x2663, # โ™ฃ BLACK CLUB SUIT - 0x2665, # โ™ฅ BLACK HEART SUIT - 0x2666, # โ™ฆ BLACK DIAMOND SUIT - 0x270C, # โœŒ VICTORY HAND - 0x2744, # โ„ SNOWFLAKE - 0x2764, # โค HEAVY BLACK HEART -} - -# TV symbols, see https://github.com/googlefonts/noto-fonts/issues/557 -TV_SYMBOLS_FOR_SUBSETTED = tool_utils.parse_int_ranges("1f19b-1f1ac 1f23b") - -EMOJI = unicode_data.get_presentation_default_emoji() | ANDROID_EMOJI - - -def _format_set(char_set, name, filename): - lines = ["%s = {" % name] - for cp in sorted(char_set): - name = unicode_data.name(cp) - lines.append(" 0x%04X, # %s %s" % (cp, unichr(cp), name)) - lines.append("}\n") - with codecs.open(filename, "w", "UTF-8") as f: - f.write("\n".join(lines)) - print("wrote", filename) - - -def subset_symbols(srcdir, dstdir): - """Subset Noto Sans Symbols in a curated way. - - Noto Sans Symbols is now subsetted in a curated way. Changes include: - - * Currency symbols now included in Roboto are removed. - - * All combining marks for symbols (except for combining keycap) are - added, to combine with other symbols if needed. - - * Characters in symbol blocks that are also covered by Noto CJK fonts - are added, for better harmony with the rest of the fonts in non-CJK - settings. The dentistry characters at U+23BE..23CC are not added, - since they appear to be Japan-only and full-width. - - * Characters that UTR #51 defines as default text are added, although - they may also exist in the color emoji font, to make sure they get - a default text style. - - * Characters that UTR #51 defines as default emoji are removed, to - make sure they don't block the fallback to the color emoji font. - - * A few math symbols that are currently included in Roboto are added, - to prepare for potentially removing them from Roboto when they are - lower-quality in Roboto. - - Based on subset_noto_sans_symbols.py from AOSP external/noto-fonts.""" - - # TODO see if we need to change this subset based on Noto Serif coverage - # (so the serif fallback chain would support them) - - target_coverage = set() - # Add all characters in BLOCKS_TO_INCLUDE - for first, last, _ in unicode_data._parse_code_ranges(BLOCKS_TO_INCLUDE): - target_coverage.update(range(first, last + 1)) - - # Add one-off characters - target_coverage |= ONE_OFF_ADDITIONS - # Remove characters preferably coming from Roboto - target_coverage -= LETTERLIKE_CHARS_IN_ROBOTO - # Remove default emoji presentation (including ones Android prefers default) - target_coverage -= EMOJI - - # Remove COMBINING ENCLOSING KEYCAP. It's needed for Android's color emoji - # mechanism to work properly - target_coverage.remove(0x20E3) - - # Remove dentistry symbols, as their main use appears to be for CJK: - # http://www.unicode.org/L2/L2000/00098-n2195.pdf - target_coverage -= set(range(0x23BE, 0x23CC + 1)) - - for font_file in glob.glob(path.join(srcdir, "NotoSansSymbols-*.ttf")): - print("main subset", font_file) - out_file = path.join(dstdir, path.basename(font_file)[:-4] + "-Subsetted.ttf") - subset.subset_font(font_file, out_file, include=target_coverage) - - # The second subset will be a fallback after the color emoji, for - # explicit text presentation sequences. - target_coverage = EMOJI | unicode_data.get_unicode_emoji_variants() - - for font_file in glob.glob(path.join(srcdir, "NotoSansSymbols-*.ttf")): - print("secondary subset", font_file) - out_file = path.join(dstdir, path.basename(font_file)[:-4] + "-Subsetted2.ttf") - subset.subset_font(font_file, out_file, include=target_coverage) - - -def patch_post_table(srcdir, dstdir): - """Replace post table version 2.0 with version 3.0""" - - # Leave alone OTF - for font_file in glob.glob(path.join(srcdir, "*.ttf")): - print("change post table to 3.0", font_file) - out_file = path.join(dstdir, path.basename(font_file)) - if path.isfile(out_file): - print(" repatching", out_file) - font_file = out_file - font = ttLib.TTFont(font_file) - font["post"].formatType = 3.0 - font.save(out_file) - - -def patch_fonts(srcdir, dstdir): - """Remove dstdir and repopulate with patched contents of srcdir (and - its 'cjk' subdirectory if it exists).""" - - srcdir = tool_utils.resolve_path(srcdir) - dstdir = tool_utils.resolve_path(dstdir) - - tool_utils.ensure_dir_exists(dstdir, clean=True) - - patch_hyphen(srcdir, dstdir) - patch_cjk_ttcs(path.join(srcdir, "cjk"), path.join(dstdir, "cjk")) - subset_symbols(srcdir, dstdir) - patch_post_table(srcdir, dstdir) - - -def main(): - SRC_DIR = "[tools]/packages/android" - DST_DIR = "[tools]/packages/android-patched" - - parser = argparse.ArgumentParser() - parser.add_argument( - "-s", - "--srcdir", - help="directory containing fonts to patch " "(default %s)" % SRC_DIR, - default=SRC_DIR, - metavar="dir", - ) - parser.add_argument( - "-d", - "--dstdir", - help="directory into which to write patched fonts " "(default %s)" % DST_DIR, - default=DST_DIR, - metavar="dir", - ) - args = parser.parse_args() - patch_fonts(args.srcdir, args.dstdir) - - -if __name__ == "__main__": - main() diff --git a/nototools/autofix_for_phase3.py b/nototools/autofix_for_phase3.py deleted file mode 100755 index a18b0ae9..00000000 --- a/nototools/autofix_for_phase3.py +++ /dev/null @@ -1,544 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2017 Google Inc. 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. - -"""Autofix phase 3 binaries, and autohint. - -Quick tool to make fixes to some fonts that are 'ok for release' but -still have issues. Main goal here is to autohint fonts that can be -hinted. We also put more info into the version string. -""" - -# TODO: ideally, we don't autofix at all, but if we continue to do this -# it would be better to unify the lint checks and the autofix code to -# ensure they agree in their expectations. - -import argparse -import datetime -import os -from os import path -import re -import subprocess - -from fontTools import ttLib - -from nototools.py23 import basestring -from nototools import font_data -from nototools import noto_data -from nototools import noto_fonts -from nototools import tool_utils - - -_new_version_re = re.compile(r"^(?:keep|[12]\.\d{3})$") - - -def _check_version(version): - if not (version is None or _new_version_re.match(version)): - raise Exception( - 'version "%s" did not match regex "%s"' % (version, _new_version_re.pattern) - ) - - -_version_info_re = re.compile( - r"GOOG;noto-(?:fonts(?:-alpha)?|source):(\d{4})(\d{2})(\d{2}):([0-9a-f]{12})" -) - - -def _check_version_info(version_info): - """ensure version info looks reasonable, for example: - 'GOOG;noto-fonts:20170220:a8a215d2e889'. Raise an exception - if it does not.""" - m = _version_info_re.match(version_info) - if not m: - raise Exception( - 'version info "%s" did not match regex "%s"' - % (version_info, _version_info_re.pattern) - ) - year = int(m.group(1)) - month = int(m.group(2)) - day = int(m.group(3)) - commit_hash = m.group(4) - today = datetime.date.today() - if 2017 <= year: - try: - encoded_date = datetime.date(year, month, day) - except Exception as e: - raise Exception( - "%04d-%02d-%02d in %s is not a valid date" - % (year, month, day, version_info) - ) - if encoded_date > today: - raise Exception( - "%s in %s is after the current date" % (encoded_date, version_info) - ) - else: - raise Exception("date in %s appears too far in the past" % version_info) - - -def _get_version_info(fonts): - """If fonts are all from noto-fonts, use information from the current - state of the repo to build a version string. Otherwise return None.""" - - # add '/' to distinguish between noto-fonts/ and noto-fonts-alpha/ - for repo_tag in ["[fonts]", "[fonts_alpha]", "[source]"]: - prefix = tool_utils.resolve_path(repo_tag) + "/" - print('trying prefix "%s"' % prefix) - if all(tool_utils.resolve_path(f).startswith(prefix) for f in fonts): - return _get_fonts_repo_version_info(repo_tag) - # else report the first failure - for f in fonts: - if not tool_utils.resolve_path(f).startswith(prefix): - print('# failed at "%s"' % tool_utils.resolve_path(f)) - break - - print("no prefix succeeded") - return None - - -def _get_fonts_repo_version_info(repo_tag): - prefix = tool_utils.resolve_path(repo_tag) - - commit, date, commit_msg = tool_utils.git_head_commit(prefix) - - # check that commit is on the upstream master - if not tool_utils.git_check_remote_commit(prefix, commit): - raise Exception( - "commit %s (%s) not on upstream master branch" - % (commit[:12], commit_msg.splitlines()[0].strip()) - ) - - date_re = re.compile(r"(\d{4})-(\d{2})-(\d{2})") - m = date_re.match(date) - if not m: - raise Exception('could not match "%s" with "%s"' % (date, date_re.pattern)) - ymd = "".join(m.groups()) - - # hack tag to get the formal repo name. strip enclosing brackets... - repo_name = "noto-" + repo_tag[1:-1].replace("_", "-") - return "GOOG;%s:%s:%s" % (repo_name, ymd, commit[:12]) - - -def _check_autohint(script): - if script and not (script in ["no-script"] or script in noto_data.HINTED_SCRIPTS): - raise Exception('not a hintable script: "%s"' % script) - - -def _expand_font_names(font_names, result=None): - """font names can include names of files containing a list of names, open - those recursively and add to the set.""" - - def strip_comment(line): - ix = line.find("#") - if ix != -1: - line = line[:ix] - return line.strip() - - if result is None: - result = set() - for n in font_names: - if not n.startswith("@"): - result.add(n) - else: - filename = n[1:] - with open(filename, "r") as f: - new_names = f.readlines() - new_names = [strip_comment(n) for n in new_names] - new_names = [n for n in new_names if n] - _expand_font_names(new_names, result) - return result - - -def autofix_fonts( - font_names, src_root, dst_dir, release_dir, version, version_info, autohint, dry_run -): - dst_dir = tool_utils.resolve_path(dst_dir) - dst_dir = tool_utils.ensure_dir_exists(dst_dir) - - font_names = sorted(_expand_font_names(font_names)) - print( - "Processing %d fonts\n %s" - % (len(font_names), "\n ".join(font_names[:5]) + "...") - ) - - src_root = tool_utils.resolve_path(src_root) - print("Src root: %s" % src_root) - print("Dest dir: %s" % dst_dir) - - if release_dir is None: - rel_dir = None - else: - rel_dir = tool_utils.resolve_path(release_dir) - if not path.isdir(rel_dir): - raise Exception('release dir "%s" does not exist' % rel_dir) - - if ( - version_info is None - or version_info == "[fonts]" - or version_info == "[fonts_alpha]" - ): - if version_info is None: - version_info = _get_version_info(font_names) - else: - version_info = _get_fonts_repo_version_info() - - if not version_info: - raise Exception("could not compute version info from fonts") - print("Computed version_info: %s" % version_info) - else: - _check_version_info(version_info) - - _check_version(version) - _check_autohint(autohint) - - if dry_run: - print("*** dry run %s***" % ("(autohint) " if autohint else "")) - for f in font_names: - f = path.join(src_root, f) - fix_font(f, dst_dir, rel_dir, version, version_info, autohint, dry_run) - - -_version_re = re.compile(r"Version (\d+\.\d{2,3})") - - -def _extract_version(font): - # Sometimes the fontRevision and version string don't match, and the - # fontRevision is bad, so we prefer the version string. - version = font_data.font_version(font) - m = _version_re.match(version) - if not m: - raise Exception('could not match existing version "%s"' % version) - return m.group(1) - - -def _version_str_to_mm(version): - # return the pair of int values for the major and minor versions, plus - # a boolean indicating whether this was a phase 2 version number. That's - # a hack, it is a phase 2 number if the initial release version < 2 or - # if the minor version had two digits. Of course this doesn't apply to - # cjk but we shouldn't run this on cjk anyway. - parts = version.split(".") - mm = [int(n) for n in parts] - is_phase2 = mm[0] < 2 or len(parts[1]) == 2 - return mm, is_phase2 - - -def _mm_to_version_str(mm): - return ("%d.%02d" if mm[0] == 1 else "%d.%03d") % tuple(mm) - - -def get_new_version(font, relfont, nversion): - """Return a new version number. font is the font we're updating, - relfont is the released version of this font if it exists, or None, - and nversion is the new version, 'keep', or None. If a new version is - passed to us, use it unless it is lower than either existing version, - in which case we raise an exception. If the version is 'keep' and - there is an existing release version, keep that. Otherwise bump the - release version, if it exists, or convert the old version to a 2.0 version - as appropriate. If the old version is a 2.0 version (e.g. Armenian was - was '2.30' in phase 2), that value is mapped to 2.40.""" - - version = _extract_version(font) - rversion = _extract_version(relfont) if relfont else None - - if rversion: - print("Existing release version: %s" % rversion) - r_mm, r_is_phase2 = _version_str_to_mm(rversion) - - mm, is_phase2 = _version_str_to_mm(version) - if nversion is not None: - if nversion == "keep": - if rversion is not None: - if r_is_phase2: - print("Warning, keeping phase 2 release version %s" % rversion) - return rversion - else: - n_mm, n_is_phase_2 = _version_str_to_mm(nversion) - if n_is_phase_2: - raise Exception('bad phase 3 minor version ("%s")' % nversion) - if rversion is not None: - if n_mm < r_mm: - raise Exception( - "new version %s < release version %s" % (nversion, rversion) - ) - if n_mm < mm: - raise Exception("new version %s < old version %s" % (nversion, version)) - return nversion - - # No new verson string, so compute one. If we have a phase 3 version, - # start with that. If it's a phase 2 number with a major version of 2, - # force minor to '040' which is higher than any of the phase 2 minor - # versions in this category. Else if major < 2, bump to 2. Else just - # bump the release minor version. - if rversion: - if r_is_phase2: - return "2.040" if r_mm[0] == 2 else "2.000" - if r_mm[1] == 999: - raise Exception("cannot bump version %s" % rversion) - r_mm[1] += 1 - return _mm_to_version_str(r_mm) - - if mm[0] > 2: - raise Exception('existing version too high "%s"' % version) - - if nversion == "keep": - return version - - return "2.000" - - -def _get_font_info(f): - font_info = noto_fonts.get_noto_font(f) - if not font_info: - raise Exception('not a noto font: "%s"' % f) - return font_info - - -def _is_ui_metrics(f): - return _get_font_info(f).is_UI_metrics - - -def _autohint_code(f, script): - """Return 'not-hinted' if we don't hint this, else return the ttfautohint - code, which might be None if ttfautohint doesn't support the script. - Note that LGC and MONO return None.""" - - if script == "no-script": - return script - if not script: - script = noto_fonts.script_key_to_primary_script(_get_font_info(f).script) - return noto_data.HINTED_SCRIPTS.get(script, "not-hinted") - - -def autohint_font(src, dst, script, dry_run): - code = _autohint_code(src, script) - if code == "not-hinted": - print("Warning: no hinting information for %s, script %s" % (src, script)) - return - - if code is None: - print("Warning: unable to autohint %s" % src) - return - - if code == "no-script": - args = ["ttfautohint", "-t", "-W", src, dst] - else: - args = ["ttfautohint", "-t", "-W", "-f", code, src, dst] - if dry_run: - print('dry run would autohint:\n "%s"' % " ".join(args)) - return - - hinted_dir = tool_utils.ensure_dir_exists(path.dirname(dst)) - try: - subprocess.check_call(args) - except Exception as e: - print("### failed to autohint %s" % src) - # we failed to autohint, let's continue anyway - # however autohint will have left an empty file there, remove it. - try: - os.remove(dst) - except: - pass - - print("wrote autohinted %s using %s" % (dst, code)) - - -def _alert(val_name, cur_val, new_val): - if isinstance(cur_val, basestring): - tmpl = 'update %s\n from: "%s"\n to: "%s"' - else: - tmpl = "update %s\n from: %4d\n to: %4d" - print(tmpl % (val_name, cur_val, new_val)) - - -def _alert_and_check(val_name, cur_val, expected_val, max_diff): - """if max_diff >= 0, curval must be <= expected_val + maxdiff, - else curval must be >= expected_val + maxdiff""" - _alert(val_name, cur_val, expected_val) - if max_diff >= 0: - err = cur_val > expected_val + max_diff - else: - err = cur_val < expected_val + max_diff - if err: - raise Exception("bad difference in expected and actual %s" % val_name) - - -def _get_release_fontpath(f, rel_dir): - """If rel_dir is not None, look for a font under 'hinted' or 'unhinted' - depending on which of these is in the path f. If neither is in f, - look under rel_dir, and then rel_dir/unhinted. If a match is found, - return the path.""" - - if rel_dir is None: - return None - - hh = True - bn = path.basename(f) - if "/hinted/" in f: - fp = path.join(rel_dir, "hinted", bn) - elif "/unhinted/" in f: - fp = path.join(rel_dir, "unhinted", bn) - else: - hh = False - fp = path.join(rel_dir, bn) - - if path.isfile(fp): - return fp - - if hh: - return None - - fp = path.join(rel_dir, "unhinted", bn) - return fp if path.isfile(fp) else None - - -def _get_release_font(f, rel_dir): - fp = _get_release_fontpath(f, rel_dir) - return None if fp is None else ttLib.TTFont(fp) - - -def fix_font(f, dst_dir, rel_dir, version, version_info, autohint, dry_run): - print("\n-----\nfont:", f) - font = ttLib.TTFont(f) - - relfont = _get_release_font(f, rel_dir) - expected_font_revision = get_new_version(font, relfont, version) - if expected_font_revision is not None: - font_revision = font_data.printable_font_revision(font, 3) - if font_revision != expected_font_revision: - _alert("revision", font_revision, expected_font_revision) - font["head"].fontRevision = float(expected_font_revision) - - names = font_data.get_name_records(font) - NAME_ID = 5 - font_version = names[NAME_ID] - expected_version = "Version %s;%s" % (expected_font_revision, version_info) - if font_version != expected_version: - _alert("version string", font_version, expected_version) - font_data.set_name_record(font, NAME_ID, expected_version) - - expected_upem = 1000 - upem = font["head"].unitsPerEm - if upem != expected_upem: - print("expected %d upem but got %d upem" % (expected_upem, upem)) - - if _is_ui_metrics(f): - if upem == 2048: - expected_ascent = 2163 - expected_descent = -555 - elif upem == 1000: - expected_ascent = 1069 - expected_descent = -293 - else: - raise Exception("no expected ui ascent/descent for upem: %d" % upem) - - font_ascent = font["hhea"].ascent - font_descent = font["hhea"].descent - if font_ascent != expected_ascent: - _alert_and_check("ascent", font_ascent, expected_ascent, 2) - font["hhea"].ascent = expected_ascent - font["OS/2"].sTypoAscender = expected_ascent - font["OS/2"].usWinAscent = expected_ascent - - if font_descent != expected_descent: - _alert_and_check("descent", font_descent, expected_descent, -2) - font["hhea"].descent = expected_descent - font["OS/2"].sTypoDescender = expected_descent - font["OS/2"].usWinDescent = -expected_descent - - tool_utils.ensure_dir_exists(path.join(dst_dir, "unhinted")) - - fname = path.basename(f) - udst = path.join(dst_dir, "unhinted", fname) - if dry_run: - print('dry run would write:\n "%s"' % udst) - else: - font.save(udst) - print("wrote %s" % udst) - - if autohint: - hdst = path.join(dst_dir, "hinted", fname) - autohint_font(udst, hdst, autohint, dry_run) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - "-d", - "--dest_dir", - help="directory into which to write swatted fonts", - metavar="dir", - default="swatted", - ) - parser.add_argument( - "-r", - "--release_dir", - help="directory containing release fonts (opt " " [fonts])", - metavar="dir", - nargs="?", - const="[fonts]", - ) - parser.add_argument( - "-f", - "--fonts", - help="paths of fonts to swat (to fetch from a file" 'use "@filename")', - metavar="font", - nargs="+", - ) - parser.add_argument( - "-s", "--src_root", help="common root of all paths of fonts", default="" - ) - parser.add_argument( - "-i", - "--version_info", - help="version info string (opt [fonts] to use " "fonts info)", - metavar="str", - nargs="?", - const="[fonts]", - ) - parser.add_argument( - "-v", - "--version", - help="force version (opt keep)", - metavar="ver", - nargs="?", - const="keep", - ) - parser.add_argument( - "-a", - "--autohint", - help="autohint fonts (opt no-script)", - metavar="code", - nargs="?", - const="no-script", - ) - parser.add_argument( - "-n", "--dry_run", help="process checks but don't fix", action="store_true" - ) - args = parser.parse_args() - - autofix_fonts( - args.fonts, - args.src_root, - args.dest_dir, - args.release_dir, - args.version, - args.version_info, - args.autohint, - args.dry_run, - ) - - -if __name__ == "__main__": - main() diff --git a/nototools/autofix_for_release.py b/nototools/autofix_for_release.py deleted file mode 100755 index 41e3d130..00000000 --- a/nototools/autofix_for_release.py +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2014 Google Inc. 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. - -"""Fix some issues in Noto fonts before releasing them.""" - -__author__ = "roozbeh@google.com (Roozbeh Pournader)" - -import argparse -import array -import os -from os import path -import re - -from fontTools import ttLib - -from nototools import font_data -from nototools import notoconfig - - -NOTO_URL = "http://www.google.com/get/noto/" - -_LICENSE_ID = 13 -_LICENSE_URL_ID = 14 - -_SIL_LICENSE = ( - "This Font Software is licensed under the SIL Open Font License, " - 'Version 1.1. This Font Software is distributed on an "AS IS" ' - "BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express " - "or implied. See the SIL Open Font License for the specific language, " - "permissions and limitations governing your use of this Font Software." -) - -_SIL_LICENSE_URL = "http://scripts.sil.org/OFL" - - -def fix_revision(font): - """Fix the revision of the font to match its version.""" - version = font_data.font_version(font) - match = re.match(r"Version (\d{1,5})\.(\d{1,5})", version) - major_version = match.group(1) - minor_version = match.group(2) - - accuracy = len(minor_version) - font_revision = font_data.printable_font_revision(font, accuracy) - expected_font_revision = major_version + "." + minor_version - if font_revision != expected_font_revision: - font["head"].fontRevision = float(expected_font_revision) - print("Fixed fontRevision to %s" % expected_font_revision) - return True - - return False - - -def fix_fstype(font): - """Fix the fsType of the font.""" - if font["OS/2"].fsType != 0: - font["OS/2"].fsType = 0 - print("Updated fsType to 0") - return True - return False - - -def fix_vendor_id(font): - """Fix the vendor ID of the font.""" - if font["OS/2"].achVendID != "GOOG": - font["OS/2"].achVendID = "GOOG" - print("Changed font vendor ID to GOOG") - return True - return False - - -# Reversed name records in Khmer and Lao fonts -NAME_CORRECTIONS = { - "Sans Kufi": "Kufi", - "SansKufi": "Kufi", - "UI Khmer": "Khmer UI", - "UIKhmer": "KhmerUI", - "UI Lao": "Lao UI", - "UILao": "LaoUI", - "SansEmoji": "Emoji", - "Sans Emoji": "Emoji", -} - -TRADEMARK_TEMPLATE = u"%s is a trademark of Google Inc." - - -def fix_name_table(font): - """Fix copyright and reversed values in the 'name' table.""" - modified = False - name_records = font_data.get_name_records(font) - - copyright_data = name_records[0] - years = re.findall("20[0-9][0-9]", copyright_data) - year = min(years) - copyright_data = u"Copyright %s Google Inc. All Rights Reserved." % year - - if copyright_data != name_records[0]: - print('Updated copyright message to "%s"' % copyright_data) - font_data.set_name_record(font, 0, copyright_data) - modified = True - - for name_id in [1, 3, 4, 6]: - record = name_records[name_id] - for source in NAME_CORRECTIONS: - if source in record: - oldrecord = record - record = record.replace(source, NAME_CORRECTIONS[source]) - break - if record != name_records[name_id]: - font_data.set_name_record(font, name_id, record) - print( - 'Updated name table record #%d from "%s" to "%s"' - % (name_id, oldrecord, record) - ) - modified = True - - trademark_names = ["Noto", "Arimo", "Tinos", "Cousine"] - trademark_name = None - font_family = name_records[1] - for name in trademark_names: - if font_family.find(name) != -1: - trademark_name = name - break - if not trademark_name: - print("no trademarked name in '%s'" % font_family) - else: - trademark_line = TRADEMARK_TEMPLATE % trademark_name - if name_records[7] != trademark_line: - old_line = name_records[7] - font_data.set_name_record(font, 7, trademark_line) - modified = True - print( - 'Updated name table record 7 from "%s" to "%s"' - % (old_line, trademark_line) - ) - - if name_records[11] != NOTO_URL: - font_data.set_name_record(font, 11, NOTO_URL) - modified = True - print('Updated name table record 11 to "%s"' % NOTO_URL) - - if name_records[_LICENSE_ID] != _SIL_LICENSE: - font_data.set_name_record(font, _LICENSE_ID, _SIL_LICENSE) - modified = True - print("Updated license id") - - if name_records[_LICENSE_URL_ID] != _SIL_LICENSE_URL: - font_data.set_name_record(font, _LICENSE_URL_ID, _SIL_LICENSE_URL) - modified = True - print("Updated license url") - - # TODO: check preferred family/subfamily(16&17) - - return modified - - -def fix_attachlist(font): - """Fix duplicate attachment points in GDEF table.""" - modified = False - try: - attach_points = font["GDEF"].table.AttachList.AttachPoint - except (KeyError, AttributeError): - attach_points = [] - - for attach_point in attach_points: - points = sorted(set(attach_point.PointIndex)) - if points != attach_point.PointIndex: - attach_point.PointIndex = points - attach_point.PointCount = len(points) - modified = True - - if modified: - print("Fixed GDEF.AttachList") - - return modified - - -def drop_hints(font): - """Drops a font's hint.""" - modified = False - glyf_table = font["glyf"] - for glyph_index in range(len(glyf_table.glyphOrder)): - glyph_name = glyf_table.glyphOrder[glyph_index] - glyph = glyf_table[glyph_name] - if glyph.numberOfContours > 0: - if glyph.program.bytecode: - glyph.program.bytecode = array.array("B") - modified = True - print('Dropped hints from glyph "%s"' % glyph_name) - return modified - - -def drop_tables(font, tables): - """Drops the listed tables from a font.""" - modified = False - for table in tables: - if table in font: - modified = True - print('Dropped table "%s"' % table) - modified = True - del font[table] - return modified - - -TABLES_TO_DROP = [ - # FontForge internal tables - "FFTM", - "PfEd", - # Microsoft VOLT internatl tables - "TSI0", - "TSI1", - "TSI2", - "TSI3", - "TSI5", - "TSID", - "TSIP", - "TSIS", - "TSIV", -] - - -def fix_path(file_path, is_hinted): - file_path = re.sub(r"_(?:un)?hinted", "", file_path) - if "hinted/" in file_path: - # '==' is higher precedence than 'in' - if ("unhinted/" in file_path) == is_hinted: - if is_hinted: - file_path = file_path.replace("unhinted/", "hinted/") - else: - file_path = file_path.replace("hinted/", "unhinted/") - else: - file_path = os.path.join("hinted" if is_hinted else "unhinted", file_path) - - # fix Naskh, assume Arabic if unspecified - file_path = re.sub(r"NotoNaskh(-|UI-)", r"NotoNaskhArabic\1", file_path) - - # fix SansEmoji - file_path = re.sub("NotoSansEmoji", "NotoEmoji", file_path) - - # fix Nastaliq - file_path = re.sub("Nastaliq-", "NastaliqUrdu-", file_path) - - return file_path - - -def fix_os2_unicoderange(font): - os2_bitmap = font_data.get_os2_unicoderange_bitmap(font) - expected_bitmap = font_data.get_cmap_unicoderange_bitmap(font) - if os2_bitmap != expected_bitmap: - old_bitmap_string = font_data.unicoderange_bitmap_to_string(os2_bitmap) - font_data.set_os2_unicoderange_bitmap(font, expected_bitmap) - bitmap_string = font_data.unicoderange_bitmap_to_string(expected_bitmap) - print( - "Change unicoderanges from:\n %s\nto:\n %s" - % (old_bitmap_string, bitmap_string) - ) - return True - return False - - -def fix_linegap(font): - modified = False - hhea_table = font["hhea"] - if hhea_table.lineGap != 0: - print("hhea lineGap was %s, setting to 0" % hhea_table.lineGap) - hhea_table.lineGap = 0 - modified = True - vhea_table = font.get("vhea") - if vhea_table and vhea_table.lineGap != 0: - print("vhea lineGap was %s, setting to 0" % vhea_table.lineGap) - vhea_table.lineGap = 0 - modified = True - os2_table = font["OS/2"] - if os2_table.sTypoLineGap != 0: - print("os/2 sTypoLineGap was %d, setting to 0" % os2_table.sTypoLineGap) - os2_table.sTypoLineGap = 0 - modified = True - return modified - - -def fix_font(src_root, dst_root, file_path, is_hinted, save_unmodified): - """Fix font under src_root and write to similar path under dst_root, modulo - fixes to the filename. If is_hinted is false, strip hints. If unmodified, - don't write destination unless save_unmodified is true.""" - - src_file = os.path.join(src_root, file_path) - - print("Font file: %s" % src_file) - font = ttLib.TTFont(src_file) - modified = False - - modified |= fix_revision(font) - modified |= fix_fstype(font) - modified |= fix_vendor_id(font) - modified |= fix_name_table(font) - modified |= fix_attachlist(font) - modified |= fix_os2_unicoderange(font) - # leave line gap for non-noto fonts alone, metrics are more constrained there - if font_data.font_name(font).find("Noto") != -1: - modified |= fix_linegap(font) - - tables_to_drop = TABLES_TO_DROP - if not is_hinted: - modified |= drop_hints(font) - tables_to_drop += ["fpgm", "prep", "cvt"] - - modified |= drop_tables(font, tables_to_drop) - - fixed_path = fix_path(file_path, is_hinted) - if fixed_path != file_path: - print('changed file_path from "%s" to "%s"' % (file_path, fixed_path)) - modified = True - - if not modified: - print("No modification necessary") - if modified or save_unmodified: - # wait until we need it before we create the dest directory - dst_file = os.path.join(dst_root, fixed_path) - dst_dir = path.dirname(dst_file) - if not path.isdir(dst_dir): - os.makedirs(dst_dir) - font.save(dst_file) - print("Wrote %s" % dst_file) - - -def fix_fonts(src_root, dst_root, name_pat, save_unmodified): - src_root = path.abspath(src_root) - dst_root = path.abspath(dst_root) - name_rx = re.compile(name_pat) - for root, dirs, files in os.walk(src_root): - for file in files: - if path.splitext(file)[1] not in [".ttf", ".ttc", ".otf"]: - continue - src_file = path.join(root, file) - file_path = src_file[len(src_root) + 1 :] # +1 to ensure no leading slash. - if not name_rx.search(file_path): - continue - is_hinted = root.endswith("/hinted") or "_hinted" in file - fix_font(src_root, dst_root, file_path, is_hinted, save_unmodified) - - -def main(): - default_src_root = notoconfig.values.get("alpha") - default_dst_root = notoconfig.values.get("autofix") - - parser = argparse.ArgumentParser() - parser.add_argument( - "name_pat", - help="regex for files to fix, " "searches relative path from src root", - ) - parser.add_argument( - "--src_root", - help="root of src files (default %s)" % default_src_root, - default=default_src_root, - ) - parser.add_argument( - "--dst_root", - help="root of destination (default %s)" % default_dst_root, - default=default_dst_root, - ) - parser.add_argument( - "--save_unmodified", help="save even unmodified files", action="store_true" - ) - args = parser.parse_args() - - if not args.src_root: - # not on command line and not in user's .notoconfig - print("no src root specified.") - return - - src_root = path.expanduser(args.src_root) - if not path.isdir(src_root): - print("%s does not exist or is not a directory" % src_root) - return - - dst_root = path.expanduser(args.dst_root) - if not path.isdir(dst_root): - print("%s does not exist or is not a directory" % dst_root) - return - - fix_fonts(src_root, dst_root, args.name_pat, args.save_unmodified) - - -if __name__ == "__main__": - main() diff --git a/nototools/fix_khmer_and_lao_coverage.py b/nototools/fix_khmer_and_lao_coverage.py deleted file mode 100755 index 360a863e..00000000 --- a/nototools/fix_khmer_and_lao_coverage.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2014 Google Inc. 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. - -"""Fix Khmer and Lao fonts for better coverage.""" - -__author__ = "roozbeh@google.com (Roozbeh Pournader)" - -import os -import sys - -from fontTools import ttLib - -from nototools import coverage -from nototools import font_data -from nototools import opentype_data - - -def merge_chars_from_bank(orig_font, bank_font, target_font, chars): - """Merge glyphs from a bank font to another font. - - Only the glyphs themselves, the horizontal metrics, and the cmaps will be - copied. - """ - bank_font = ttLib.TTFont(bank_font) - orig_font = ttLib.TTFont(orig_font) - - bank_cmap = font_data.get_cmap(bank_font) - extra_cmap = {} - for char in sorted(chars): - assert char in bank_cmap - bank_glyph_name = bank_cmap[char] - assert bank_glyph_name not in orig_font["glyf"].glyphs - orig_font["glyf"][bank_glyph_name] = bank_font["glyf"][bank_glyph_name] - orig_font["hmtx"][bank_glyph_name] = bank_font["hmtx"][bank_glyph_name] - extra_cmap[char] = bank_glyph_name - font_data.add_to_cmap(orig_font, extra_cmap) - orig_font.save(target_font) - - -_UNHINTED_FONTS_DIR = os.path.abspath( - os.path.join( - os.path.dirname(__file__), os.pardir, "fonts", "individual", "unhinted" - ) -) - - -def main(argv): - """Fix all the fonts given in the command line. - - If they are Lao fonts, make sure they have ZWSP and dotted circle. If they - are Khmer fonts, make sure they have ZWSP, joiners, and dotted circle.""" - - for font_name in argv[1:]: - if "Khmer" in font_name: - script = "Khmr" - elif "Lao" in font_name: - script = "Laoo" - needed_chars = set(opentype_data.SPECIAL_CHARACTERS_NEEDED[script]) - - lgc_font_name = ( - os.path.basename(font_name).replace("Khmer", "").replace("Lao", "") - ) - lgc_font_name = os.path.join(_UNHINTED_FONTS_DIR, lgc_font_name) - - font_charset = coverage.character_set(font_name) - missing_chars = needed_chars - font_charset - if missing_chars: - merge_chars_from_bank( - font_name, - lgc_font_name, - os.path.dirname(font_name) + "/new/" + os.path.basename(font_name), - missing_chars, - ) - - -if __name__ == "__main__": - main(sys.argv) diff --git a/nototools/fix_noto_cjk_thin.py b/nototools/fix_noto_cjk_thin.py deleted file mode 100755 index fcf66b49..00000000 --- a/nototools/fix_noto_cjk_thin.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2014 Google Inc. 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. - -"""Fix usWeight problem in Noto CJK Thin OTF fonts.""" - -__author__ = "roozbeh@google.com (Roozbeh Pournader)" - -import sys - -from fontTools import ttLib - -# Increase Version (name table fields 3 and 5, head.fontRevision) -# Change name field 10 to mention we've changed the font -# Change usWeight to 250 - - -def fix_font(source_filename): - """Create a Windows-specific version of the font.""" - assert source_filename.endswith(".otf") - font = ttLib.TTFont(source_filename) - - name_table = font["name"] - for record in name_table.names: - if record.platformID == 1: # Mac - assert record.platEncID == 0 - assert record.langID == 0 - encoding = "Mac-Roman" - else: # Windows - assert record.platformID == 3 - assert record.platEncID == 1 - assert record.langID == 0x0409 - encoding = "UTF-16BE" - value = record.string.decode(encoding) - if record.nameID == 3: - original_version = value[: value.index(";")] - new_version = original_version + "1" - new_value = value.replace(original_version, new_version, 1) - - # Replace the unique identifier to avoid version conflicts - assert new_value.endswith("ADOBE") - new_value = new_value.replace("ADBE", "GOOG", 1) - new_value = new_value.replace("ADOBE", "GOOGLE", 1) - assert new_value.endswith("GOOGLE") - - assert new_value != value - record.string = new_value.encode(encoding) - elif record.nameID == 5: - new_value = value.replace(original_version, new_version, 1) - assert new_value != value - record.string = new_value.encode(encoding) - elif record.nameID == 10: - # record #10 appears to be the best place to put a change notice - assert "Google" not in value - new_value = value + ( - "; Changed by Google " "to work around a bug in Windows" - ) - record.string = new_value.encode(encoding) - - font["head"].fontRevision = float(new_version) - - assert font["OS/2"].usWeightClass == 100 - font["OS/2"].usWeightClass = 250 - - target_filename = source_filename.replace(".otf", "-Windows.otf") - font.save(target_filename) - - -def main(argv): - """Fix all fonts provided in the command line.""" - for font_filename in argv[1:]: - fix_font(font_filename) - - -if __name__ == "__main__": - main(sys.argv) diff --git a/nototools/swat_license.py b/nototools/swat_license.py deleted file mode 100755 index caa7c1af..00000000 --- a/nototools/swat_license.py +++ /dev/null @@ -1,483 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2015 Google Inc. 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. - -"""Swat copyright, bump version.""" - - -import argparse -import collections -import os -from os import path -import re - -from nototools import autofix_for_release -from nototools import cldr_data -from nototools import font_data -from nototools import noto_fonts -from nototools import ttc_utils - -from fontTools import ttLib -from fontTools import misc - -_COPYRIGHT_ID = 0 -_VERSION_ID = 5 -_TRADEMARK_ID = 7 -_MANUFACTURER_ID = 8 -_DESIGNER_ID = 9 -_DESCRIPTION_ID = 10 -_VENDOR_URL_ID = 11 -_DESIGNER_URL_ID = 12 -_LICENSE_ID = 13 -_LICENSE_URL_ID = 14 - -_NAME_ID_LABELS = { - _COPYRIGHT_ID: "copyright", - _VERSION_ID: "version", - _TRADEMARK_ID: "trademark", - _MANUFACTURER_ID: "manufacturer", - _DESIGNER_ID: "designer", - _DESCRIPTION_ID: "description", - _VENDOR_URL_ID: "vendor url", - _DESIGNER_URL_ID: "designer url", - _LICENSE_ID: "license", - _LICENSE_URL_ID: "license url", -} - -_SIL_LICENSE = ( - "This Font Software is licensed under the SIL Open Font License, " - 'Version 1.1. This Font Software is distributed on an "AS IS" ' - "BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express " - "or implied. See the SIL Open Font License for the specific language, " - "permissions and limitations governing your use of this Font Software." -) - -_SIL_LICENSE_URL = "http://scripts.sil.org/OFL" - -_NOTO_URL = "http://www.google.com/get/noto/" - -_SCRIPT_KEYS = {"Aran": "Urdu", "HST": "Historic", "LGC": ""} - -_FAMILY_KEYS = { - "Arimo": "a", - "Cousine": "b", - "Tinos": "c", - "Noto": "d", -} - -_HINTED_TABLES_TO_DROP = autofix_for_release.TABLES_TO_DROP -_UNHINTED_TABLES_TO_DROP = autofix_for_release.TABLES_TO_DROP + ["fpgm", "prep", "cvt"] - -_changes = {} - -_autofix = collections.defaultdict(list) - -_ttc_fonts = {} - - -def _swat_fonts(dst_root, dry_run): - def family_key(family): - return _FAMILY_KEYS.get(family, "x" + family) - - def script_key(script): - return _SCRIPT_KEYS.get(script, None) or cldr_data.get_english_script_name( - script - ) - - def compare_key(font): - return ( - family_key(font.family), - font.style, - script_key(font.script), - "a" if font.is_hinted else "", - font.variant if font.variant else "", - "UI" if font.is_UI else "", - "" if font.weight == "Regular" else font.weight, - font.slope or "", - font.fmt, - ) - - fonts = noto_fonts.get_noto_fonts() - for font in sorted(fonts, key=compare_key): - _swat_font(font, dst_root, dry_run) - - if _ttc_fonts: - _construct_ttc_fonts(fonts, dst_root, dry_run) - - -def _noto_relative_path(filepath): - """Return relative path from some noto root, or None""" - x = filepath.find("noto-fonts") - if x == -1: - x = filepath.find("noto-cjk") - if x == -1: - x = filepath.find("noto-emoji") - if x == -1: - return None - return filepath[x:] - - -def get_bumped_version(ttfont, is_hinted=None): - """Return bumped values for the header and name tables.""" - - names = font_data.get_name_records(ttfont) - version = names[_VERSION_ID] - m = re.match(r"Version (\d{1,5})\.(\d{1,5})( uh)?(;.*)?", version) - if not m: - print("! Could not match version string (%s)" % version) - return None, None - - major_version = m.group(1) - minor_version = m.group(2) - print('old version: "%s"' % version) - if is_hinted is None: - is_hinted = not bool(m.group(3)) - print("computed hinted = %s" % is_hinted) - - version_remainder = m.group(4) - accuracy = len(minor_version) - print_revision = font_data.printable_font_revision(ttfont, accuracy) - # sanity check - expected_revision = major_version + "." + minor_version - if expected_revision != print_revision: - raise ValueError( - "! Expected revision '%s' but got revision '%s'" - % (expected_revision, print_revision) - ) - - # bump the minor version keeping significant digits: - new_minor_version = str(int(minor_version) + 1).zfill(accuracy) - new_revision = major_version + "." + new_minor_version - print("Update revision from '%s' to '%s'" % (expected_revision, new_revision)) - # double check we are going to properly round-trip this value - float_revision = float(new_revision) - fixed_revision = misc.fixedTools.floatToFixed(float_revision, 16) - rt_float_rev = misc.fixedTools.fixedToFloat(fixed_revision, 16) - rt_float_rev_int = int(rt_float_rev) - rt_float_rev_frac = int(round((rt_float_rev - rt_float_rev_int) * 10 ** accuracy)) - rt_new_revision = ( - str(rt_float_rev_int) + "." + str(rt_float_rev_frac).zfill(accuracy) - ) - if new_revision != rt_new_revision: - raise ValueError( - "! Could not update new revision, expected '%s' but got '%s'" - % (new_revision, rt_new_revision) - ) - - new_version_string = "Version " + new_revision - if not is_hinted: - new_version_string += " uh" - if version_remainder: - new_version_string += version_remainder - - return float_revision, new_version_string - - -def _swat_font(noto_font, dst_root, dry_run): - filepath = noto_font.filepath - basename = path.basename(filepath) - if noto_font.is_cjk: - print("# Skipping cjk font %s" % basename) - return - if noto_font.fmt == "ttc": - print("# Deferring ttc font %s" % basename) - _ttc_fonts[noto_font] = ttc_utils.ttcfile_filenames(filepath) - return - - ttfont = ttLib.TTFont(filepath, fontNumber=0) - - names = font_data.get_name_records(ttfont) - - # create relative root path - rel_filepath = _noto_relative_path(filepath) - if not rel_filepath: - raise ValueError("Could not identify noto root of %s" % filepath) - - print("-----\nUpdating %s" % rel_filepath) - - dst_file = path.join(dst_root, rel_filepath) - - try: - new_revision, new_version_string = get_bumped_version( - ttfont, noto_font.is_hinted - ) - except ValueError as e: - print(e) - return - - print("%s: %s" % ("Would write" if dry_run else "Writing", dst_file)) - - new_trademark = "%s is a trademark of Google Inc." % noto_font.family - - # description field should be set. - # Roozbeh has note, make sure design field has information - # on whether the font is hinted. - # Missing in Lao and Khmer, default in Cham. - if cldr_data.get_english_script_name(noto_font.script) in ["Lao", "Khmer", "Cham"]: - new_description = "Data %shinted." % ("" if noto_font.is_hinted else "un") - # elif noto_font.vendor is 'Monotype': - elif not noto_font.is_cjk and noto_font.family == "Noto": - new_description = "Data %shinted. Designed by Monotype design team." % ( - "" if noto_font.is_hinted else "un" - ) - else: - new_description = None - - if re.match( - r"^Copyright 20[12]\d Google (Inc|LLC). All Rights Reserved\.$", - names[_COPYRIGHT_ID], - ): - new_copyright = None - else: - new_copyright = "!!" - - if names.get(_DESIGNER_ID) in [ - "Steve Matteson", - "Monotype Design Team", - "Danh Hong", - ]: - new_designer = None - elif names.get(_DESIGNER_ID) == "Monotype Design team": - new_designer = "Monotype Design Team" - elif ( - _DESIGNER_ID not in names - and cldr_data.get_english_script_name(noto_font.script) == "Khmer" - ): - new_designer = "Danh Hong" - else: - new_designer = "!!" - - if names.get(_DESIGNER_URL_ID) in [ - "http://www.monotype.com/studio", - "http://www.khmertype.org", - ]: - new_designer_url = None - elif names.get(_DESIGNER_URL_ID) in [ - "http://www.monotypeimaging.com/ProductsServices/TypeDesignerShowcase", - ]: - new_designer_url = "http://www.monotype.com/studio" - elif names.get(_DESIGNER_URL_ID) in [ - "http://www.khmertype.blogspot.com", - "http://www.khmertype.blogspot.com/", - "http://khmertype.blogspot.com/", - "http://wwwkhmertype.blogspot.com.com/", - ]: - new_designer_url = "http://www.khmertype.org" - else: - new_designer_url = "!!!" - - if names.get(_MANUFACTURER_ID) in [ - "Monotype Imaging Inc.", - "Danh Hong", - "Google LLC", - "Google Inc.", - ]: - new_manufacturer = None - else: - new_manufacturer = "!!!" - - def update(name_id, new, newText=None): - old = names.get(name_id) - if new and (new != old): - if not dry_run and not "!!!" in new: - font_data.set_name_record(ttfont, name_id, new, addIfMissing="win") - - label = _NAME_ID_LABELS[name_id] - oldText = "'%s'" % old if old else "None" - newText = newText or ("'%s'" % new) - print("%s:\n old: %s\n new: %s" % (label, oldText, newText or new)) - - label_change = _changes.get(label) - if not label_change: - label_change = {} - _changes[label] = label_change - new_val_change = label_change.get(new) - if not new_val_change: - new_val_change = {} - label_change[new] = new_val_change - old_val_fonts = new_val_change.get(old) - if not old_val_fonts: - old_val_fonts = [] - new_val_change[old] = old_val_fonts - old_val_fonts.append(noto_font.filepath) - - update(_COPYRIGHT_ID, new_copyright) - update(_VERSION_ID, new_version_string) - update(_TRADEMARK_ID, new_trademark) - update(_MANUFACTURER_ID, new_manufacturer) - update(_DESIGNER_ID, new_designer) - update(_DESCRIPTION_ID, new_description) - update(_VENDOR_URL_ID, _NOTO_URL) - update(_DESIGNER_URL_ID, new_designer_url) - update(_LICENSE_ID, _SIL_LICENSE, newText="(OFL)") - update(_LICENSE_URL_ID, _SIL_LICENSE_URL) - - if autofix_for_release.fix_fstype(ttfont): - _autofix["fstype"].append(noto_font.filepath) - if autofix_for_release.fix_vendor_id(ttfont): - _autofix["vendor_id"].append(noto_font.filepath) - if autofix_for_release.fix_attachlist(ttfont): - _autofix["attachlist"].append(noto_font.filepath) - if noto_font.is_hinted: - tables_to_drop = _HINTED_TABLES_TO_DROP - else: - tables_to_drop = _UNHINTED_TABLES_TO_DROP - if autofix_for_release.drop_hints(ttfont): - _autofix["drop_hints"].append(noto_font.filepath) - if autofix_for_release.drop_tables(ttfont, tables_to_drop): - _autofix["drop_tables"].append(noto_font.filepath) - if noto_font.family == "Noto": - if autofix_for_release.fix_linegap(ttfont): - _autofix["linegap"].append(noto_font.filepath) - if autofix_for_release.fix_os2_unicoderange(ttfont): - _autofix["os2_unicoderange"].append(noto_font.filepath) - - if dry_run: - return - - ttfont["head"].fontRevision = float_revision - - dst_dir = path.dirname(dst_file) - if not path.isdir(dst_dir): - os.makedirs(dst_dir) - ttfont.save(dst_file) - print("Wrote file.") - - -def _construct_ttc_fonts(fonts, dst_root, dry_run): - # _ttc_fonts contains a map from a font path to a list of likely names - # of the component fonts. The component names are based off the - # postscript name in the name table of the component, so 1) might not - # accurately represent the font, and 2) don't indicate whether the - # component is hinted. We deal with the former by rejecting and - # reporting ttcs where any name fails to match, and with the latter - # by assuming all the components are hinted or not based on whether - # the original is in a 'hinted' or 'unhinted' directory. - - # build a map from basename to a list of noto_font objects - basename_to_fonts = collections.defaultdict(list) - for font in fonts: - if font.fmt != "ttc": - basename = path.basename(font.filepath) - basename_to_fonts[basename].append(font) - - for ttcfont, components in sorted(_ttc_fonts.items()): - rel_filepath = _noto_relative_path(ttcfont.filepath) - print("-----\nBuilding %s" % rel_filepath) - - component_list = [] - # note the component order must match the original ttc, so - # we must process in the provided order. - for component in components: - possible_components = basename_to_fonts.get(component) - if not possible_components: - print( - "! no match for component named %s in %s" - % (component, rel_filepath) - ) - component_list = [] - break - - matched_possible_component = None - for possible_component in possible_components: - if possible_component.is_hinted == ttcfont.is_hinted: - if matched_possible_component: - print( - "! already matched possible component %s for %s" - % ( - matched_possible_component.filename, - possible_component.filename, - ) - ) - matched_possible_component = None - break - matched_possible_component = possible_component - if not matched_possible_component: - print("no matched component named %s" % component) - component_list = [] - break - component_list.append(matched_possible_component) - if not component_list: - print("! cannot generate ttc font %s" % rel_filepath) - continue - - print( - "components:\n " - + "\n ".join(_noto_relative_path(font.filepath) for font in component_list) - ) - if dry_run: - continue - - dst_ttc = path.join(dst_root, rel_filepath) - src_files = [ - path.join(dst_root, _noto_relative_path(font.filepath)) - for font in component_list - ] - ttc_utils.build_ttc(dst_ttc, src_files) - print("Built %s" % dst_ttc) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - "-n", "--dry_run", help="Do not write fonts", action="store_true" - ) - parser.add_argument( - "--dst_root", - help="root of destination (default /tmp/swat)", - metavar="dst", - default="/tmp/swat", - ) - parser.add_argument("--details", help="show change details", action="store_true") - args = parser.parse_args() - - _swat_fonts(args.dst_root, args.dry_run) - - print("------\nchange summary\n") - for name_key in sorted(_changes): - print("%s:" % name_key) - new_vals = _changes[name_key] - for new_val in sorted(new_vals): - print(" change to '%s':" % new_val) - old_vals = new_vals[new_val] - for old_val in sorted(old_vals): - print( - " from %s (%d files)%s" - % ( - "'%s'" % old_val if old_val else "None", - len(old_vals[old_val]), - ":" if args.details else "", - ) - ) - if args.details: - for file_name in sorted(old_vals[old_val]): - x = file_name.rfind("/") - if x > 0: - x = file_name.rfind("/", 0, x) - print(" " + file_name[x:]) - - print("------\nautofix summary\n") - for fix_key in sorted(_autofix): - fixed_files = _autofix[fix_key] - print("%s (%d):" % (fix_key, len(fixed_files))) - for file_name in sorted(fixed_files): - x = file_name.rfind("/") - if x > 0: - x = file_name.rfind("/", 0, x) - print(" " + file_name[x:]) - - -if __name__ == "__main__": - main()