Skip to content
This repository has been archived by the owner on Jan 25, 2023. It is now read-only.

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
rsheeter committed Jan 13, 2022
1 parent e14ef80 commit d03da67
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 59 deletions.
123 changes: 114 additions & 9 deletions android_connection/apply_to_android.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
from absl import app
from absl import flags
import filecmp
from lxml import etree
from pathlib import Path
import sys
import shutil


FLAGS = flags.FLAGS


flags.DEFINE_string("android_root", None, "Root of android repo")
flags.DEFINE_bool("dry_run", True, "If False actually update files")


def _require_exists(a_file: Path) -> Path:
if not a_file.is_file():
raise ValueError(f"{a_file} not found")
raise ValueError(f"{a_file} missing or not a file")
return a_file


def _require_dir_exists(a_dir: Path) -> Path:
if not a_dir.is_dir():
raise ValueError(f"{a_dir} missing or not a dir")
return a_dir


def _fonts_xml(android_dir: Path) -> Path:
return _require_exists(android_dir / "frameworks" / "base" / "data" / "fonts" / "fonts.xml")

Expand All @@ -16,18 +33,106 @@ def _fonts_mk(android_dir: Path) -> Path:
return _require_exists(android_dir / "external" / "noto-fonts" / "fonts.mk")


def _font_dir(android_dir: Path) -> Path:
return _require_dir_exists(android_dir / "external" / "noto-fonts" / "other")


def _repo_root() -> Path:
root = (Path(__file__).parent.parent).absolute()
if not (root / "LICENSE").is_file():
raise IOError(f"{root} does not contain LICENSE")
return root


def noto_4_android_path() -> Path:
xml_file = _repo_root() / "android_connection" / "noto-fonts-4-android.xml"
if not xml_file.is_file():
raise IOError(f"No file {xml_file}")
return xml_file


def font_file(font_el) -> str:
return ("".join(font_el.itertext())).strip()


def font_path(font_el) -> Path:
name = font_file(font_el)
path = font_el.attrib["path"]
return _require_exists(_repo_root() / path / name)


def _validate_android_path(android_dir: Path):
assert android_dir.is_dir(), f"{android_dir} should be a directory"
_fonts_xml(android_dir) # just to trigger it's checks
_fonts_mk(android_dir) # just to trigger it's checks
# just to trigger existance validation
_fonts_xml(android_dir)
_fonts_mk(android_dir)
_font_dir(android_dir)


def main():
if len(sys.argv) != 2:
raise ValueError("Must have one arg, path to an Android checkout")
android_dir = Path(sys.argv[1])
def main(_):
if not FLAGS.android_root:
raise ValueError("Must provide --android_root")
android_dir = Path(FLAGS.android_root)
_validate_android_path(android_dir)

# gather fonts that should be copied to Android
noto_for_android = etree.parse(str(noto_4_android_path()))
new_paths = {}
for font_el in noto_for_android.xpath("//font[@path]"):
path = font_path(font_el)
if new_paths.get(path.name, path) != path:
raise IOError(f"Multiple paths for {path.name}")
new_paths[path.name] = path
old_paths = {p.name: p for p in _font_dir(android_dir).glob("Noto*.[ot]t[fc]")}

new_names = set(new_paths.keys())
old_names = set(old_paths.keys())

delta_sz = 0

deleted_files = old_names - new_names
print(f"{len(deleted_files)} DELETED")
for delete_me in sorted(deleted_files):
print(f" {delete_me}")
if not FLAGS.dry_run:
old_paths[delete_me].unlink()
del delete_me
del deleted_files
print()

added_files = new_names - old_names
print(f"{len(added_files)} ADDED")
for add_me in sorted(added_files):
dest = _font_dir(android_dir) / add_me
print(f" {add_me}")
if not FLAGS.dry_run:
shutil.copy(new_paths[add_me], dest)
del add_me
del added_files
print()

updated_files = new_names & old_names
untouched = 0
print(f"{len(updated_files)} UPDATED")
for update_me in sorted(updated_files):
if filecmp.cmp(new_paths[update_me], old_paths[update_me], shallow=False):
untouched += 1
continue

dest = _font_dir(android_dir) / update_me
print(f" {update_me}")
if not FLAGS.dry_run:
shutil.copy(new_paths[update_me], dest)
del update_me
del updated_files
print()

print(f"{untouched} did not change")
print()

print(f"Done updating files, you should manually update {_fonts_xml(android_dir)}"
f" from {noto_4_android_path()}")


if __name__ == "__main__":
main()
app.run(main)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
absl-py>=0.9.0
fonttools>=4.28.5
lxml>=4.7.1
pytest>=6.2.5
14 changes: 8 additions & 6 deletions tests/apply_to_android_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from android_connection import apply_to_android
from android_connection.apply_to_android import _validate_android_path
from lxml import etree
from pathlib import Path
import pytest

Expand All @@ -11,10 +12,11 @@ def _fake_android_dir() -> Path:
return _testdata_dir() / "fake_android"


def test_apply_to_bad_dir():
with pytest.raises(ValueError, match="not found"):
apply_to_android._validate_android_path(_testdata_dir())
def test_validate_bad_dir():
with pytest.raises(ValueError, match="missing"):
_validate_android_path(_testdata_dir())


def test_apply_to_good_dir():
apply_to_android._validate_android_path(_fake_android_dir())
def test_validate_good_dir():
_validate_android_path(_fake_android_dir())

70 changes: 26 additions & 44 deletions tests/noto_fonts_for_android_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
from pathlib import Path
import pytest
from typing import Tuple
from android_connection.apply_to_android import (
font_file,
font_path,
noto_4_android_path,
)


_KNOWN_PATHLESS = {
Expand All @@ -14,37 +19,12 @@
}



def _repo_root() -> Path:
root = (Path(__file__).parent / "..").absolute()
if not (root / "LICENSE").is_file():
raise IOError(f"{root} does not contain LICENSE")
return root


def _noto_4_android_file() -> Path:
xml_file = _repo_root() / "android_connection" / "noto-fonts-4-android.xml"
if not xml_file.is_file():
raise IOError(f"No file {xml_file}")
return xml_file


def _font_file(font_el) -> str:
return ("".join(font_el.itertext())).strip()


def _font_path(font_el) -> Path:
name = _font_file(font_el)
path = font_el.attrib["path"]
return _repo_root() / path / name


def _is_collection(font_el) -> bool:
return _font_file(font_el).lower().endswith(".ttc")
return font_file(font_el).lower().endswith(".ttc")


def _open_font(font_el) -> ttLib.TTFont:
path = _font_path(font_el)
path = font_path(font_el)
if not path.is_file():
raise IOError(f"No such file: {path}")

Expand Down Expand Up @@ -80,35 +60,35 @@ def _weight(font: ttLib.TTFont) -> Tuple[int, int, int]:


def test_fonts_have_path():
root = etree.parse(str(_noto_4_android_file()))
root = etree.parse(str(noto_4_android_path()))
bad = []
for font in root.iter("font"):
font_file = _font_file(font)
if font_file in _KNOWN_PATHLESS:
assert "path" not in font.attrib, f"{font_file} not expected to have path. Correct _KNOWN_PATHLESS if you just added path"
name = font_file(font)
if name in _KNOWN_PATHLESS:
assert "path" not in font.attrib, f"{name} not expected to have path. Correct _KNOWN_PATHLESS if you just added path"
continue

if not font.attrib.get("path", ""):
bad.append(font_file)
bad.append(name)
assert not bad, "Missing path attribute: " + ", ".join(bad)


def test_ttcs_have_index():
root = etree.parse(str(_noto_4_android_file()))
root = etree.parse(str(noto_4_android_path()))
bad = []
for font in root.iter("font"):
if not _is_collection(font):
continue
if "index" not in font.attrib:
bad.append(_font_file(font))
bad.append(font_file(font))
assert not bad, "Missing index attribute: " + ", ".join(bad)


def test_font_paths_are_valid():
root = etree.parse(str(_noto_4_android_file()))
root = etree.parse(str(noto_4_android_path()))
bad = []
for font in root.xpath("//font[@path]"):
path = _font_path(font)
path = font_path(font)
if not path.is_file():
bad.append(str(path))
assert not bad, "No such file: " + ", ".join(bad)
Expand All @@ -120,39 +100,41 @@ def test_font_weights():
"NotoNastaliqUrdu-Bold.ttf weight 700 outside font capability 400..400",
"NotoSerifMyanmar-Bold.ttf weight 700 outside font capability 400..400"
}
root = etree.parse(str(_noto_4_android_file()))
root = etree.parse(str(noto_4_android_path()))
errors = []
for font_el in root.xpath("//font[@path]"):
xml_weight = int(font_el.attrib["weight"])
path = _font_path(font_el)
path = font_path(font_el)

font = _open_font(font_el)
min_wght, default_wght, max_weight = _weight(font)

if xml_weight < min_wght or xml_weight > max_weight:
error_str = f"{_font_file(font_el)} weight {xml_weight} outside font capability {min_wght}..{max_weight}"
error_str = f"{font_file(font_el)} weight {xml_weight} outside font capability {min_wght}..{max_weight}"
if error_str not in expected_errors:
errors.append(error_str)

assert not errors, ", ".join(errors)


def test_font_full_weight_coverage():
root = etree.parse(str(_noto_4_android_file()))
root = etree.parse(str(noto_4_android_path()))
errors = []
for family in root.iter("family"):
font_to_xml_weights = collections.defaultdict(set)
for font in family.xpath("//font[@path]"):
font_to_xml_weights[(_font_path(font), font.attrib.get("index", -1))].add(int(font.attrib["weight"]))
path = font_path(font)
ttc_idx = font.attrib.get("index", -1)
font_to_xml_weights[(path, ttc_idx)].add(int(font.attrib["weight"]))

# now you have a map of font path => set of weights in xml
for (font_path, font_number), xml_weights in font_to_xml_weights.items():
for (path, ttc_idx), xml_weights in font_to_xml_weights.items():
# open the font, compute the 100 weights between it's min/max weight
# if xml_weights != computed weights add this to the error list
font = _open_font_path(font_path, font_number)
font = _open_font_path(path, ttc_idx)
min_wght, default_wght, max_weight = _weight(font)
if min(xml_weights) > min_wght or max(xml_weights) < max_weight:
errors.append(f"{font_path} weight range {min(xml_weights)}..{max(xml_weights)} could be expanded to {min_wght}..{max_weight}")
errors.append(f"{path} weight range {min(xml_weights)}..{max(xml_weights)} could be expanded to {min_wght}..{max_weight}")

assert not errors, ", ".join(errors)

Expand Down
Empty file.

0 comments on commit d03da67

Please sign in to comment.