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

Script to update Android (DRAFT) #2206

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Empty file added android_connection/__init__.py
Empty file.
138 changes: 138 additions & 0 deletions android_connection/apply_to_android.py
@@ -0,0 +1,138 @@
from absl import app
from absl import flags
import filecmp
from lxml import etree
from pathlib import Path
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} 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")


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"
# just to trigger existance validation
_fonts_xml(android_dir)
_fonts_mk(android_dir)
_font_dir(android_dir)


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__":
app.run(main)
1 change: 1 addition & 0 deletions requirements.txt
@@ -1,3 +1,4 @@
absl-py>=0.9.0
fonttools>=4.28.5
lxml>=4.7.1
pytest>=6.2.5
Empty file added tests/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions tests/apply_to_android_test.py
@@ -0,0 +1,22 @@
from android_connection.apply_to_android import _validate_android_path
from lxml import etree
from pathlib import Path
import pytest


def _testdata_dir() -> Path:
return Path(__file__).parent / "testdata"


def _fake_android_dir() -> Path:
return _testdata_dir() / "fake_android"


def test_validate_bad_dir():
with pytest.raises(ValueError, match="missing"):
_validate_android_path(_testdata_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
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.