Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 43c3b56

Browse files
madigfelipesanches
authored andcommitted
Implement a basic tool to update regression test files.
See #4589 for details. Run like `python -m fontbakery.update_shaping_tests input.toml output.json path/to/*.ttf`. (PR #4603)
1 parent 085799a commit 43c3b56

4 files changed

Lines changed: 215 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A more detailed list of changes is available in the corresponding milestones for
99
- v0.12.0a2 (2024-Feb-21)
1010
- v0.12.0a3 (2024-Mar-13)
1111
- v0.12.0a4 (2024-Mar-15)
12+
- Implement a basic tool to update regression test files. See https://github.com/fonttools/fontbakery/discussions/4589 for details. Run like `python -m fontbakery.update_shaping_tests input.toml output.json path/to/*.ttf`.
1213

1314

1415
## 0.12.0a4 (2024-Mar-15)
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Copyright 2020 Google Sans Authors
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+
15+
"""Update a regression test file with the shaping output of a list of fonts."""
16+
17+
from __future__ import annotations
18+
19+
import enum
20+
import sys
21+
from pathlib import Path
22+
from typing import Any, Dict, List, Optional, TypedDict
23+
24+
import vharfbuzz as vhb # type: ignore
25+
from fontTools.ttLib import TTFont # type: ignore
26+
from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r # type: ignore
27+
28+
if sys.version_info >= (3, 11):
29+
import tomllib
30+
from typing import NotRequired
31+
32+
TOMLDecodeError = tomllib.TOMLDecodeError
33+
else:
34+
import toml as tomllib
35+
from typing_extensions import NotRequired
36+
37+
TOMLDecodeError = tomllib.TomlDecodeError
38+
39+
40+
def main(args: List[str] | None = None) -> None:
41+
import argparse
42+
import json
43+
44+
parser = argparse.ArgumentParser()
45+
parser.add_argument(
46+
"shaping_file", type=Path, help="The .toml shaping definition input file path."
47+
)
48+
parser.add_argument(
49+
"output_file",
50+
type=Path,
51+
help="The .json shaping expectations output file path.",
52+
)
53+
parser.add_argument(
54+
"fonts",
55+
nargs="+",
56+
type=Path,
57+
help="The fonts to update the testing file with.",
58+
)
59+
parsed_args = parser.parse_args(args)
60+
61+
input_path: Path = parsed_args.shaping_file
62+
output_path: Path = parsed_args.output_file
63+
fonts: List[Path] = parsed_args.fonts
64+
65+
shaping_input = load_shaping_input(input_path)
66+
shaping_output = update_shaping_output(shaping_input, fonts)
67+
output_path.write_text(json.dumps(shaping_output, indent=2, ensure_ascii=False))
68+
69+
70+
def update_shaping_output(
71+
shaping_input: ShapingInput, font_paths: List[Path]
72+
) -> ShapingOutput:
73+
tests: List[TestDefinition] = []
74+
75+
for font_path in font_paths:
76+
shaper = vhb.Vharfbuzz(font_path)
77+
font = TTFont(font_path)
78+
for text in shaping_input["text"]:
79+
if "fvar" in font:
80+
fvar: table__f_v_a_r = font["fvar"] # type: ignore
81+
for instance in fvar.instances:
82+
run = shape_run(
83+
shaper,
84+
font_path,
85+
text,
86+
shaping_input,
87+
instance.coordinates,
88+
)
89+
tests.append(run)
90+
else:
91+
run = shape_run(shaper, font_path, text, shaping_input)
92+
tests.append(run)
93+
94+
return {"tests": tests}
95+
96+
97+
def shape_run(
98+
shaper: vhb.Vharfbuzz,
99+
font_path: Path,
100+
text: str,
101+
shaping_input: ShapingInput,
102+
variations: Optional[Dict[str, float]] = None,
103+
) -> TestDefinition:
104+
parameters: VHarfbuzzParameters = {}
105+
if (script := shaping_input.get("script")) is not None:
106+
parameters["script"] = script
107+
if (direction := shaping_input.get("direction")) is not None:
108+
parameters["direction"] = direction.value
109+
if (language := shaping_input.get("language")) is not None:
110+
parameters["language"] = language
111+
if features := shaping_input.get("features"):
112+
parameters["features"] = features
113+
if variations:
114+
parameters["variations"] = variations
115+
buffer = shaper.shape(text, parameters)
116+
117+
shaping_comparison_mode = shaping_input["comparison_mode"]
118+
if shaping_comparison_mode is ComparisonMode.FULL:
119+
glyphsonly = False
120+
elif shaping_comparison_mode is ComparisonMode.GLYPHSTREAM:
121+
glyphsonly = True
122+
else:
123+
raise ValueError(f"Unknown comparison mode {shaping_comparison_mode}.")
124+
expectation = shaper.serialize_buf(buffer, glyphsonly)
125+
126+
test_definition: TestDefinition = {
127+
"only": font_path.name,
128+
"input": text,
129+
"expectation": expectation,
130+
**parameters,
131+
}
132+
133+
return test_definition
134+
135+
136+
def load_shaping_input(input_path: Path) -> ShapingInput:
137+
with input_path.open("rb") as tf:
138+
try:
139+
shaping_input: ShapingInputToml = tomllib.load(tf) # type: ignore
140+
except TOMLDecodeError as e:
141+
raise ValueError(
142+
f"{input_path} does not contain a parseable shaping input."
143+
) from e
144+
145+
if "input" not in shaping_input:
146+
raise ValueError(f"{input_path} does not contain a valid shaping input.")
147+
148+
input_definition = shaping_input["input"]
149+
input_definition["text"] = input_definition.get("text", [])
150+
input_definition["script"] = input_definition.get("script")
151+
input_definition["language"] = input_definition.get("language")
152+
input_definition["direction"] = (
153+
Direction(input_definition["direction"])
154+
if "direction" in input_definition
155+
else None
156+
)
157+
input_definition["features"] = input_definition.get("features", {})
158+
input_definition["comparison_mode"] = ComparisonMode(
159+
input_definition.get("comparison_mode", "full")
160+
)
161+
162+
return input_definition
163+
164+
165+
class ShapingInputToml(TypedDict):
166+
input: ShapingInput
167+
168+
169+
class ShapingInput(TypedDict):
170+
text: List[str]
171+
script: Optional[str]
172+
language: Optional[str]
173+
direction: Optional[Direction]
174+
features: Dict[str, bool]
175+
comparison_mode: ComparisonMode
176+
177+
178+
class ComparisonMode(enum.Enum):
179+
FULL = "full" # Record glyph names, offsets and advance widths.
180+
GLYPHSTREAM = "glyphstream" # Just glyph names.
181+
182+
183+
class Direction(enum.Enum):
184+
LEFT_TO_RIGHT = "ltr"
185+
RIGHT_TO_LEFT = "rtl"
186+
TOP_TO_BOTTOM = "ttb"
187+
BOTTOM_TO_TOP = "btt"
188+
189+
190+
class ShapingOutput(TypedDict):
191+
configuration: NotRequired[Dict[str, Any]]
192+
tests: List[TestDefinition]
193+
194+
195+
class VHarfbuzzParameters(TypedDict, total=False):
196+
script: str
197+
direction: str
198+
language: str
199+
features: Dict[str, bool]
200+
variations: Dict[str, float]
201+
202+
203+
class TestDefinition(VHarfbuzzParameters):
204+
input: str
205+
expectation: str
206+
only: NotRequired[str]
207+
208+
209+
if __name__ == "__main__":
210+
main()

examples/shaping.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ com.google.fonts/check/shaping:
2424
After saving this file — as `shaping.yml`, for example — you can then run
2525
the Shaping profile checks using the following command:
2626

27-
fontbakery shaping --config shaping.yml Font.ttf
27+
fontbakery check-shaping --config shaping.yml Font.ttf
2828

2929
For best results, generate an HTML report using the `--html` option.
3030

31-
fontbakery shaping --config shaping.yml --html shaping.html Font.ttf
31+
fontbakery check-shaping --config shaping.yml --html shaping.html Font.ttf
3232

3333
The report will include SVG illustrations for any failing tests.
3434

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ build-backend = "setuptools.build_meta"
66
name = "fontbakery"
77
dynamic = ["version"]
88
description = "A font quality assurance tool for everyone"
9+
requires-python = ">=3.8"
910
readme = { file = "README.md", content-type = "text/markdown" }
1011
authors = [
1112
{ name = "Chris Simpkins", email = "[email protected]" },
@@ -43,6 +44,7 @@ dependencies = [
4344
"beziers >= 0.5.0, == 0.5.*",
4445
"uharfbuzz",
4546
"vharfbuzz >= 0.2.0, == 0.2.*",
47+
"typing_extensions ; python_version < '3.11'",
4648
]
4749

4850
[project.optional-dependencies]

0 commit comments

Comments
 (0)