123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- # Copyright 2020 Google LLC
- #
- # 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.
- """Quick and dirty utility to download latest icon assets for github."""
- from absl import app
- from absl import flags
- import icons
- import json
- from pathlib import Path
- import re
- import requests
- import time
- from typing import NamedTuple, Set, Sequence, Tuple
- from zipfile import ZipFile
- from joblib import Parallel, delayed, wrap_non_picklable_objects
- from fontTools.ttLib import woff2
- import os
- FLAGS = flags.FLAGS
- flags.DEFINE_bool("fetch", True, "Whether we can attempt to download assets.")
- flags.DEFINE_integer("icon_limit", 0, "If > 0, the max # of icons to process.")
- _METADATA_URL = "http://fonts.google.com/metadata/icons?incomplete=1&key=material_symbols"
- class Asset(NamedTuple):
- src_url_pattern: str
- dest_dir_pattern: str
- class Fetch(NamedTuple):
- src_url: str
- dest_file: Path
- class Icon(NamedTuple):
- name: str
- version: int
- stylistic_sets: Set[str]
- _ICON_ASSETS = (
- Asset(
- "https://{host}/s/i/short-term/release/{stylistic_set_snake}/{icon.name}/{style}/{size_px}px.svg",
- "symbols/web/{icon.name}/{stylistic_set_snake}/{icon.name}{style_suffix}_{size_px}px.svg",
- ),
- Asset(
- "https://{host}/s/i/short-term/release/{stylistic_set_snake}/{icon.name}/{style}/{size_px}px.xml",
- "symbols/android/{icon.name}/{stylistic_set_snake}/{icon.name}{style_suffix}_{size_px}px.xml",
- ),
- )
- # no wght variants for apple symbols.
- _ICON_IOS_ASSETS = (
- Asset(
- "https://{host}/s/i/short-term/release/{stylistic_set_snake}/{icon.name}/{style}/{icon.name}{style_suffix}_symbol.svg",
- "symbols/ios/{icon.name}/{stylistic_set_snake}/{icon.name}{style_suffix}_symbol.svg"
- ),
- )
- _HEADERS = {
- 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
- }
- _SET_ASSETS = (
- # Fonts are acquired by abusing the Google Fonts web api. Nobody tell them :D
- Asset(
- "https://fonts.googleapis.com/css2?family={stylistic_set_url}:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200",
- "variablefont/{stylistic_set_font}.css",
- ),
- )
- def _latest_metadata():
- resp = requests.get(_METADATA_URL)
- resp.raise_for_status()
- raw_json = resp.text[5:]
- return json.loads(raw_json)
- def _current_versions():
- return Path("current_versions.json")
- def _version_key(icon: Icon):
- return f"symbols::{icon.name}"
- def _symbol_families(metadata):
- return set(s for s in set(metadata["families"]) if "Symbols" in s)
- def _icons(metadata):
- all_sets = _symbol_families(metadata)
- for raw_icon in metadata["icons"]:
- unsupported = set(raw_icon["unsupported_families"])
- yield Icon(
- raw_icon["name"],
- raw_icon["version"],
- all_sets - unsupported,
- )
- def _create_fetch(asset, args):
- src_url = asset.src_url_pattern.format(**args)
- dest_file = asset.dest_dir_pattern.format(**args)
- dest_file = (Path(__file__) / "../.." / dest_file).resolve()
- return Fetch(src_url, dest_file)
- @delayed
- def _do_fetch_delayed(src_url, dest_file, i, total):
- _do_fetch(src_url, dest_file)
- if i % 5000 == 0:
- print("%d/%d complete" % (i, total))
- def _do_fetch(src_url, dest_file):
- try :
- resp = requests.get(src_url, headers = _HEADERS)
- resp.raise_for_status()
- dest_file.parent.mkdir(parents=True, exist_ok=True)
- dest_file.write_bytes(resp.content)
- except Exception as e:
- print(str(e))
- def _do_fetches(fetches):
- print(f"Starting {len(fetches)} fetches")
- total = len(fetches)
- Parallel(n_jobs=50)(_do_fetch_delayed(f.src_url, f.dest_file, i, total) for i,f in enumerate(fetches))
- if total:
- print("%d/%d complete" % (total, total))
-
- def decompress(infilepath: Path, outfilepath: Path):
- with infilepath.open(mode='rb') as infile:
- with outfilepath.open(mode='wb') as outfile:
- woff2.decompress(infile, outfile)
- def _fetch_fonts(css_files: Sequence[Path]):
- for css_file in css_files:
- css = css_file.read_text()
- url = re.search(r"src:\s+url\(([^)]+)\)", css).group(1)
- assert url.endswith(".woff2")
- woff2_file = css_file.parent / (css_file.stem + ".woff2")
- dest_file = css_file.parent / (css_file.stem + ".ttf")
- _do_fetch(url, woff2_file)
- decompress(woff2_file, dest_file)
- css_file.unlink()
- with open(dest_file.with_suffix(".codepoints"), "w") as f:
- for name, codepoint in sorted(icons.enumerate(dest_file)):
- f.write(f"{name} {codepoint:04x}\n")
- def _is_css(p: Path):
- return p.suffix == ".css"
- def _files(fetches: Sequence[Fetch], pred):
- return [f.dest_file for f in fetches if pred(f.dest_file)]
- def _should_skip(fetch: Fetch):
- return fetch.dest_file.is_file()
- def _pattern_args(metadata, stylistic_set):
- return {
- "host": metadata["host"],
- "stylistic_set_snake": stylistic_set.replace(" ", "").lower(),
- "stylistic_set_url": stylistic_set.replace(" ", "+"),
- "stylistic_set_font": stylistic_set.replace(" ", "") + "[FILL,GRAD,opsz,wght]",
- }
- def _create_fetches(style, opsz, pattern_args, fetches, skips, assets):
- pattern_args["style"] = style if style else "default"
- pattern_args["style_suffix"] = f"_{style}" if style else ""
- pattern_args["size_px"] = str(opsz)
-
- for asset in assets:
- fetch = _create_fetch(asset, pattern_args)
- if _should_skip(fetch):
- skips.append(fetch)
- else:
- fetches.append(fetch)
- def main(_):
- current_versions = json.loads(_current_versions().read_text())
- metadata = _latest_metadata()
- stylistic_sets = _symbol_families(metadata)
- fetches = []
- skips = []
- num_changed = 0
- icons = tuple(_icons(metadata))
- if FLAGS.icon_limit > 0:
- icons = icons[: FLAGS.icon_limit]
- for icon in icons:
- ver_key = _version_key(icon)
- if icon.version <= current_versions.get(ver_key, 0):
- continue
- current_versions[ver_key] = icon.version
- num_changed += 1
- for stylistic_set in stylistic_sets:
- if stylistic_set not in icon.stylistic_sets:
- continue
- pattern_args = _pattern_args(metadata, stylistic_set)
- pattern_args["icon"] = icon
- for opsz in [20,24,40,48] :
- for fill in ["","fill1"] :
- for grad in ["gradN25","","grad200"]:
- _create_fetches(grad + fill, opsz, pattern_args, fetches, skips, _ICON_IOS_ASSETS)
- for wght in ["wght100","wght200","wght300","","wght500","wght600","wght700"] :
- _create_fetches(wght + grad + fill, opsz, pattern_args, fetches, skips, _ICON_ASSETS)
- for stylistic_set in stylistic_sets:
- for asset in _SET_ASSETS:
- pattern_args = _pattern_args(metadata, stylistic_set)
- fetch = _create_fetch(asset, pattern_args)
- fetches.append(fetch)
- print(f"{num_changed}/{len(icons)} icons have changed")
- if skips:
- print(f"{len(skips)} fetches skipped because assets exist")
- if fetches:
- if FLAGS.fetch:
- _do_fetches(fetches)
- else:
- print(f"fetch disabled; not fetching {len(fetches)} assets")
- _fetch_fonts(_files(fetches + skips, _is_css))
- with open(_current_versions(), "w") as f:
- json.dump(current_versions, f, indent=4, sort_keys=True)
- if __name__ == "__main__":
- app.run(main)
|