mirror of
https://github.com/google/material-design-icons.git
synced 2026-02-20 00:51:36 +01:00
259 lines
8.1 KiB
Python
259 lines
8.1 KiB
Python
# 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_bool("overwrite", False, "Update and overwrite existing 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 not FLAGS.overwrite and 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 not FLAGS.overwrite and 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)
|