|
| 1 | +import logging |
| 2 | +import shutil |
| 3 | +from os import PathLike |
| 4 | +from pathlib import Path |
| 5 | + |
| 6 | +import chws_tool |
| 7 | +import httpx |
| 8 | +from fontTools import ttLib |
| 9 | +from nototools import font_data, tool_utils |
| 10 | +from tqdm import tqdm |
| 11 | + |
| 12 | +## BEGIN: https://android.googlesource.com/platform/external/noto-fonts.git/+/refs/heads/android15-release/scripts/subset_noto_cjk.py |
| 13 | +# Characters supported in Noto CJK fonts that UTR #51 recommends default to |
| 14 | +# emoji-style. |
| 15 | +EMOJI_IN_CJK = { |
| 16 | + 0x26BD, # ⚽ SOCCER BALL |
| 17 | + 0x26BE, # ⚾ BASEBALL |
| 18 | + 0x1F18E, # 🆎 NEGATIVE SQUARED AB |
| 19 | + 0x1F191, # 🆑 SQUARED CL |
| 20 | + 0x1F192, # 🆒 SQUARED COOL |
| 21 | + 0x1F193, # 🆓 SQUARED FREE |
| 22 | + 0x1F194, # 🆔 SQUARED ID |
| 23 | + 0x1F195, # 🆕 SQUARED NEW |
| 24 | + 0x1F196, # 🆖 SQUARED NG |
| 25 | + 0x1F197, # 🆗 SQUARED OK |
| 26 | + 0x1F198, # 🆘 SQUARED SOS |
| 27 | + 0x1F199, # 🆙 SQUARED UP WITH EXCLAMATION MARK |
| 28 | + 0x1F19A, # 🆚 SQUARED VS |
| 29 | + 0x1F201, # 🈁 SQUARED KATAKANA KOKO |
| 30 | + 0x1F21A, # 🈚 SQUARED CJK UNIFIED IDEOGRAPH-7121 |
| 31 | + 0x1F22F, # 🈯 SQUARED CJK UNIFIED IDEOGRAPH-6307 |
| 32 | + 0x1F232, # 🈲 SQUARED CJK UNIFIED IDEOGRAPH-7981 |
| 33 | + 0x1F233, # 🈳 SQUARED CJK UNIFIED IDEOGRAPH-7A7A |
| 34 | + 0x1F234, # 🈴 SQUARED CJK UNIFIED IDEOGRAPH-5408 |
| 35 | + 0x1F235, # 🈵 SQUARED CJK UNIFIED IDEOGRAPH-6E80 |
| 36 | + 0x1F236, # 🈶 SQUARED CJK UNIFIED IDEOGRAPH-6709 |
| 37 | + 0x1F238, # 🈸 SQUARED CJK UNIFIED IDEOGRAPH-7533 |
| 38 | + 0x1F239, # 🈹 SQUARED CJK UNIFIED IDEOGRAPH-5272 |
| 39 | + 0x1F23A, # 🈺 SQUARED CJK UNIFIED IDEOGRAPH-55B6 |
| 40 | + 0x1F250, # 🉐 CIRCLED IDEOGRAPH ADVANTAGE |
| 41 | + 0x1F251, # 🉑 CIRCLED IDEOGRAPH ACCEPT |
| 42 | +} |
| 43 | +# Characters we have decided we are doing as emoji-style in Android, |
| 44 | +# despite UTR #51's recommendation |
| 45 | +ANDROID_EMOJI = { |
| 46 | + 0x2600, # ☀ BLACK SUN WITH RAYS |
| 47 | + 0x2601, # ☁ CLOUD |
| 48 | + 0x260E, # ☎ BLACK TELEPHONE |
| 49 | + 0x261D, # ☝ WHITE UP POINTING INDEX |
| 50 | + 0x263A, # ☺ WHITE SMILING FACE |
| 51 | + 0x2660, # ♠ BLACK SPADE SUIT |
| 52 | + 0x2663, # ♣ BLACK CLUB SUIT |
| 53 | + 0x2665, # ♥ BLACK HEART SUIT |
| 54 | + 0x2666, # ♦ BLACK DIAMOND SUIT |
| 55 | + 0x270C, # ✌ VICTORY HAND |
| 56 | + 0x2744, # ❄ SNOWFLAKE |
| 57 | + 0x2764, # ❤ HEAVY BLACK HEART |
| 58 | +} |
| 59 | +# We don't want support for ASCII control chars. |
| 60 | +CONTROL_CHARS = set(tool_utils.parse_int_ranges("0000-001F")) |
| 61 | +EXCLUDED_CODEPOINTS = frozenset(sorted(EMOJI_IN_CJK | ANDROID_EMOJI | CONTROL_CHARS)) |
| 62 | + |
| 63 | + |
| 64 | +def remove_codepoints_from_ttc(ttc_path, out_dir): |
| 65 | + """Removes a set of characters from a TTC font file's cmap table.""" |
| 66 | + logging.info("Loading %s", ttc_path) |
| 67 | + ttc = ttLib.TTCollection(ttc_path) |
| 68 | + logging.info("Subsetting %d fonts in the collection", len(ttc)) |
| 69 | + for font in ttc: |
| 70 | + font_data.delete_from_cmap(font, EXCLUDED_CODEPOINTS) |
| 71 | + out_path = out_dir / ttc_path.name |
| 72 | + logging.info("Saving to %s", out_path) |
| 73 | + ttc.save(out_path) |
| 74 | + logging.info( |
| 75 | + "Size: %d --> %d, delta=%d", |
| 76 | + ttc_path.stat().st_size, |
| 77 | + out_path.stat().st_size, |
| 78 | + out_path.stat().st_size - ttc_path.stat().st_size, |
| 79 | + ) |
| 80 | + |
| 81 | + |
| 82 | +## END: https://android.googlesource.com/platform/external/noto-fonts.git/+/refs/heads/android15-release/scripts/subset_noto_cjk.py |
| 83 | + |
| 84 | + |
| 85 | +def download_file( |
| 86 | + url: str, save_path_file_name: str | bytes | PathLike[str] | PathLike[bytes] |
| 87 | +) -> bool: |
| 88 | + with open(save_path_file_name, "wb") as f: |
| 89 | + with httpx.stream("GET", url, follow_redirects=True) as response: |
| 90 | + if response.status_code != 200: |
| 91 | + logging.error(f"Failed to download {url}") |
| 92 | + return False |
| 93 | + with tqdm( |
| 94 | + total=int(response.headers.get("content-length", 0)), |
| 95 | + unit="B", |
| 96 | + unit_divisor=1024, |
| 97 | + unit_scale=True, |
| 98 | + ) as progress: |
| 99 | + num_bytes_downloaded = response.num_bytes_downloaded |
| 100 | + for chunk in response.iter_bytes(): |
| 101 | + f.write(chunk) |
| 102 | + progress.update( |
| 103 | + response.num_bytes_downloaded - num_bytes_downloaded |
| 104 | + ) |
| 105 | + num_bytes_downloaded = response.num_bytes_downloaded |
| 106 | + return True |
| 107 | + |
| 108 | + |
| 109 | +def download_and_patch_noto_cjk_font(url): |
| 110 | + base_file_name = url.split("/")[-1] |
| 111 | + ## Download |
| 112 | + logging.info(f"Downloading {url}...") |
| 113 | + input_dir = Path("temp/input") |
| 114 | + input_dir.mkdir(parents=True, exist_ok=True) |
| 115 | + input_file = input_dir / base_file_name |
| 116 | + if not download_file(url, input_file): |
| 117 | + logging.error("Failed to download") |
| 118 | + return |
| 119 | + |
| 120 | + ## CHWS Patch |
| 121 | + logging.info("Applying CHWS patch...") |
| 122 | + output_path = Path("temp/chws_output") |
| 123 | + output_path.mkdir(exist_ok=True) |
| 124 | + output_file = output_path / base_file_name |
| 125 | + chws_tool.add_chws(input_file, output_file) |
| 126 | + |
| 127 | + ## Subset |
| 128 | + logging.info("Subsetting...") |
| 129 | + result_path = Path("system/fonts") |
| 130 | + result_path.mkdir(parents=True, exist_ok=True) |
| 131 | + remove_codepoints_from_ttc(output_file, result_path) |
| 132 | + logging.info("Done!") |
| 133 | + shutil.rmtree(Path("temp")) |
0 commit comments