update_symbols.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. # Copyright 2020 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Quick and dirty utility to download latest icon assets for github."""
  15. from absl import app
  16. from absl import flags
  17. import icons
  18. import json
  19. from pathlib import Path
  20. import re
  21. import requests
  22. import time
  23. from typing import NamedTuple, Set, Sequence, Tuple
  24. from zipfile import ZipFile
  25. from joblib import Parallel, delayed, wrap_non_picklable_objects
  26. from fontTools.ttLib import woff2
  27. import os
  28. FLAGS = flags.FLAGS
  29. flags.DEFINE_bool("fetch", True, "Whether we can attempt to download assets.")
  30. flags.DEFINE_bool("overwrite", False, "Update and overwrite existing assets.")
  31. flags.DEFINE_integer("icon_limit", 0, "If > 0, the max # of icons to process.")
  32. _METADATA_URL = "http://fonts.google.com/metadata/icons?incomplete=1&key=material_symbols"
  33. class Asset(NamedTuple):
  34. src_url_pattern: str
  35. dest_dir_pattern: str
  36. class Fetch(NamedTuple):
  37. src_url: str
  38. dest_file: Path
  39. class Icon(NamedTuple):
  40. name: str
  41. version: int
  42. stylistic_sets: Set[str]
  43. _ICON_ASSETS = (
  44. Asset(
  45. "https://{host}/s/i/short-term/release/{stylistic_set_snake}/{icon.name}/{style}/{size_px}px.svg",
  46. "symbols/web/{icon.name}/{stylistic_set_snake}/{icon.name}{style_suffix}_{size_px}px.svg",
  47. ),
  48. Asset(
  49. "https://{host}/s/i/short-term/release/{stylistic_set_snake}/{icon.name}/{style}/{size_px}px.xml",
  50. "symbols/android/{icon.name}/{stylistic_set_snake}/{icon.name}{style_suffix}_{size_px}px.xml",
  51. ),
  52. )
  53. # no wght variants for apple symbols.
  54. _ICON_IOS_ASSETS = (
  55. Asset(
  56. "https://{host}/s/i/short-term/release/{stylistic_set_snake}/{icon.name}/{style}/{icon.name}{style_suffix}_symbol.svg",
  57. "symbols/ios/{icon.name}/{stylistic_set_snake}/{icon.name}{style_suffix}_symbol.svg"
  58. ),
  59. )
  60. _HEADERS = {
  61. 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
  62. }
  63. _SET_ASSETS = (
  64. # Fonts are acquired by abusing the Google Fonts web api. Nobody tell them :D
  65. Asset(
  66. "https://fonts.googleapis.com/css2?family={stylistic_set_url}:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200",
  67. "variablefont/{stylistic_set_font}.css",
  68. ),
  69. )
  70. def _latest_metadata():
  71. resp = requests.get(_METADATA_URL)
  72. resp.raise_for_status()
  73. raw_json = resp.text[5:]
  74. return json.loads(raw_json)
  75. def _current_versions():
  76. return Path("current_versions.json")
  77. def _version_key(icon: Icon):
  78. return f"symbols::{icon.name}"
  79. def _symbol_families(metadata):
  80. return set(s for s in set(metadata["families"]) if "Symbols" in s)
  81. def _icons(metadata):
  82. all_sets = _symbol_families(metadata)
  83. for raw_icon in metadata["icons"]:
  84. unsupported = set(raw_icon["unsupported_families"])
  85. yield Icon(
  86. raw_icon["name"],
  87. raw_icon["version"],
  88. all_sets - unsupported,
  89. )
  90. def _create_fetch(asset, args):
  91. src_url = asset.src_url_pattern.format(**args)
  92. dest_file = asset.dest_dir_pattern.format(**args)
  93. dest_file = (Path(__file__) / "../.." / dest_file).resolve()
  94. return Fetch(src_url, dest_file)
  95. @delayed
  96. def _do_fetch_delayed(src_url, dest_file, i, total):
  97. _do_fetch(src_url, dest_file)
  98. if i % 5000 == 0:
  99. print("%d/%d complete" % (i, total))
  100. def _do_fetch(src_url, dest_file):
  101. try :
  102. resp = requests.get(src_url, headers = _HEADERS)
  103. resp.raise_for_status()
  104. dest_file.parent.mkdir(parents=True, exist_ok=True)
  105. dest_file.write_bytes(resp.content)
  106. except Exception as e:
  107. print(str(e))
  108. def _do_fetches(fetches):
  109. print(f"Starting {len(fetches)} fetches")
  110. total = len(fetches)
  111. Parallel(n_jobs=50)(_do_fetch_delayed(f.src_url, f.dest_file, i, total) for i,f in enumerate(fetches))
  112. if total:
  113. print("%d/%d complete" % (total, total))
  114. def decompress(infilepath: Path, outfilepath: Path):
  115. with infilepath.open(mode='rb') as infile:
  116. with outfilepath.open(mode='wb') as outfile:
  117. woff2.decompress(infile, outfile)
  118. def _fetch_fonts(css_files: Sequence[Path]):
  119. for css_file in css_files:
  120. css = css_file.read_text()
  121. url = re.search(r"src:\s+url\(([^)]+)\)", css).group(1)
  122. assert url.endswith(".woff2")
  123. woff2_file = css_file.parent / (css_file.stem + ".woff2")
  124. dest_file = css_file.parent / (css_file.stem + ".ttf")
  125. _do_fetch(url, woff2_file)
  126. decompress(woff2_file, dest_file)
  127. css_file.unlink()
  128. with open(dest_file.with_suffix(".codepoints"), "w") as f:
  129. for name, codepoint in sorted(icons.enumerate(dest_file)):
  130. f.write(f"{name} {codepoint:04x}\n")
  131. def _is_css(p: Path):
  132. return p.suffix == ".css"
  133. def _files(fetches: Sequence[Fetch], pred):
  134. return [f.dest_file for f in fetches if pred(f.dest_file)]
  135. def _should_skip(fetch: Fetch):
  136. return not FLAGS.overwrite and fetch.dest_file.is_file()
  137. def _pattern_args(metadata, stylistic_set):
  138. return {
  139. "host": metadata["host"],
  140. "stylistic_set_snake": stylistic_set.replace(" ", "").lower(),
  141. "stylistic_set_url": stylistic_set.replace(" ", "+"),
  142. "stylistic_set_font": stylistic_set.replace(" ", "") + "[FILL,GRAD,opsz,wght]",
  143. }
  144. def _create_fetches(style, opsz, pattern_args, fetches, skips, assets):
  145. pattern_args["style"] = style if style else "default"
  146. pattern_args["style_suffix"] = f"_{style}" if style else ""
  147. pattern_args["size_px"] = str(opsz)
  148. for asset in assets:
  149. fetch = _create_fetch(asset, pattern_args)
  150. if _should_skip(fetch):
  151. skips.append(fetch)
  152. else:
  153. fetches.append(fetch)
  154. def main(_):
  155. current_versions = json.loads(_current_versions().read_text())
  156. metadata = _latest_metadata()
  157. stylistic_sets = _symbol_families(metadata)
  158. fetches = []
  159. skips = []
  160. num_changed = 0
  161. icons = tuple(_icons(metadata))
  162. if FLAGS.icon_limit > 0:
  163. icons = icons[: FLAGS.icon_limit]
  164. for icon in icons:
  165. ver_key = _version_key(icon)
  166. if not FLAGS.overwrite and icon.version <= current_versions.get(ver_key, 0):
  167. continue
  168. current_versions[ver_key] = icon.version
  169. num_changed += 1
  170. for stylistic_set in stylistic_sets:
  171. if stylistic_set not in icon.stylistic_sets:
  172. continue
  173. pattern_args = _pattern_args(metadata, stylistic_set)
  174. pattern_args["icon"] = icon
  175. for opsz in [20,24,40,48] :
  176. for fill in ["","fill1"] :
  177. for grad in ["gradN25","","grad200"]:
  178. _create_fetches(grad + fill, opsz, pattern_args, fetches, skips, _ICON_IOS_ASSETS)
  179. for wght in ["wght100","wght200","wght300","","wght500","wght600","wght700"] :
  180. _create_fetches(wght + grad + fill, opsz, pattern_args, fetches, skips, _ICON_ASSETS)
  181. for stylistic_set in stylistic_sets:
  182. for asset in _SET_ASSETS:
  183. pattern_args = _pattern_args(metadata, stylistic_set)
  184. fetch = _create_fetch(asset, pattern_args)
  185. fetches.append(fetch)
  186. print(f"{num_changed}/{len(icons)} icons have changed")
  187. if skips:
  188. print(f"{len(skips)} fetches skipped because assets exist")
  189. if fetches:
  190. if FLAGS.fetch:
  191. _do_fetches(fetches)
  192. else:
  193. print(f"fetch disabled; not fetching {len(fetches)} assets")
  194. _fetch_fonts(_files(fetches + skips, _is_css))
  195. with open(_current_versions(), "w") as f:
  196. json.dump(current_versions, f, indent=4, sort_keys=True)
  197. if __name__ == "__main__":
  198. app.run(main)