"""
Checks for suitability for in-car displays (ISO 15008).
"""
from beziers.line import Line
from beziers.path import BezierPath
from beziers.point import Point
from fontTools.pens.boundsPen import BoundsPen
from fontbakery.callable import check
from fontbakery.fonts_profile import profile_factory
from fontbakery.message import Message
from fontbakery.section import Section
from fontbakery.status import PASS, FAIL
from fontbakery.utils import exit_with_install_instructions
try:
import uharfbuzz as hb
except ImportError:
exit_with_install_instructions()
profile_imports = ((".", ("shared_conditions",)),)
profile = profile_factory(default_section=Section("Suitability for In-Car Display"))
DISCLAIMER = """
(Note that passing this check does not guarantee compliance with ISO-15008.)
"""
CHECKS = [
"com.google.fonts/check/iso15008_proportions",
"com.google.fonts/check/iso15008_stem_width",
"com.google.fonts/check/iso15008_intercharacter_spacing",
"com.google.fonts/check/iso15008_interword_spacing",
"com.google.fonts/check/iso15008_interline_spacing",
]
[docs]def xheight_intersections(ttFont, glyph):
glyphset = ttFont.getGlyphSet()
if glyph not in glyphset:
return []
paths = BezierPath.fromFonttoolsGlyph(ttFont, glyph)
if len(paths) != 1:
return []
path = paths[0]
xheight = ttFont["OS/2"].sxHeight
bounds = path.bounds()
bounds.addMargin(10)
ray = Line(Point(bounds.left, xheight), Point(bounds.right, xheight))
intersections = []
for seg in path.asSegments():
intersections.extend(seg.intersections(ray))
return sorted(intersections, key=lambda i: i.point.x)
[docs]def stem_width(ttFont):
glyphset = ttFont.getGlyphSet()
if "l" not in glyphset:
return None
intersections = xheight_intersections(ttFont, "l")
if len(intersections) != 2:
return None
(i1, i2) = intersections[0:2]
return abs(i1.point.x - i2.point.x)
[docs]def pair_kerning(font, left, right):
"""The kerning between two glyphs (specified by name), in font units."""
with open(font, "rb") as fontfile:
fontdata = fontfile.read()
face = hb.Face(fontdata)
font = hb.Font(face)
scale = face.upem
font.scale = (scale, scale)
buf = hb.Buffer()
buf.add_str(left + right)
buf.guess_segment_properties()
hb.shape(font, buf, {"kern": True})
pos = buf.glyph_positions[0].x_advance
buf = hb.Buffer()
buf.add_str(left + right)
buf.guess_segment_properties()
hb.shape(font, buf, {"kern": False})
pos2 = buf.glyph_positions[0].x_advance
return pos - pos2
[docs]@check(
id="com.google.fonts/check/iso15008_proportions",
rationale="""
According to ISO 15008, fonts used for in-car displays should not be
too narrow or too wide.
To ensure legibility of this font on in-car information systems,
it is recommended that the ratio of H width to H height
is between 0.65 and 0.80.
"""
+ DISCLAIMER,
proposal=[
"https://github.com/fonttools/fontbakery/issues/1832",
"https://github.com/fonttools/fontbakery/issues/3250",
],
)
def com_google_fonts_check_iso15008_proportions(ttFont):
"""Check if 0.65 => (H width / H height) => 0.80"""
glyphset = ttFont.getGlyphSet()
if "H" not in glyphset:
yield FAIL, Message(
"glyph-not-present",
"There was no 'H' glyph in the font,"
" so the proportions could not be tested",
)
return
pen = BoundsPen(glyphset)
glyphset["H"].draw(pen)
(xMin, yMin, xMax, yMax) = pen.bounds
proportion = (xMax - xMin) / (yMax - yMin)
if 0.65 <= proportion <= 0.80:
yield PASS, "the letter H is not too narrow or too wide"
else:
yield FAIL, Message(
"invalid-proportion",
f"The proportion of H width to H height ({proportion})"
f"does not conform to the expected range of 0.65-0.80",
)
[docs]@check(
id="com.google.fonts/check/iso15008_stem_width",
rationale="""
According to ISO 15008, fonts used for in-car displays should
not be too light or too bold.
To ensure legibility of this font on in-car information systems,
it is recommended that the ratio of stem width to ascender height
is between 0.10 and 0.20.
"""
+ DISCLAIMER,
proposal=[
"https://github.com/fonttools/fontbakery/issues/1832",
"https://github.com/fonttools/fontbakery/issues/3251",
],
)
def com_google_fonts_check_iso15008_stem_width(ttFont):
"""Check if 0.10 <= (stem width / ascender) <= 0.82"""
width = stem_width(ttFont)
if width is None:
yield FAIL, Message("no-stem-width", "Could not determine stem width")
return
ascender = ttFont["hhea"].ascender
proportion = width / ascender
if 0.10 <= proportion <= 0.20:
yield PASS, "the stem width is not too light or too bold"
else:
yield FAIL, Message(
"invalid-proportion",
f"The proportion of stem width to ascender ({proportion})"
f"does not conform to the expected range of 0.10-0.20",
)
[docs]@check(
id="com.google.fonts/check/iso15008_intercharacter_spacing",
rationale="""
According to ISO 15008, fonts used for in-car displays should not
be too narrow or too wide.
To ensure legibility of this font on in-car information systems,
it is recommended that the spacing falls within the following values:
* space between vertical strokes (e.g. "ll") should be 150%-240%
of the stem width.
* space between diagonals and verticals (e.g. "vl") should be
at least 85% of the stem width.
* diagonal characters should not touch (e.g. "vv").
"""
+ DISCLAIMER,
proposal=[
"https://github.com/fonttools/fontbakery/issues/1832",
"https://github.com/fonttools/fontbakery/issues/3252",
],
)
def com_google_fonts_check_iso15008_intercharacter_spacing(font, ttFont):
"""Check if spacing between characters is adequate for display use"""
width = stem_width(ttFont)
# Because an l can have a curly tail, we don't want the *glyph* sidebearings;
# we want the sidebearings measured using a line at Y=x-height.
l_intersections = xheight_intersections(ttFont, "l")
if width is None or len(l_intersections) < 2:
yield FAIL, Message("no-stem-width", "Could not determine stem width")
return
l_lsb = l_intersections[0].point.x
l_advance = ttFont["hmtx"]["l"][0]
l_rsb = l_advance - l_intersections[-1].point.x
l_l = l_rsb + pair_kerning(font, "l", "l") + l_lsb
if l_l is None:
yield FAIL, Message(
"glyph-not-present",
"There was no 'l' glyph in the font, so the spacing could not be tested",
)
return
if 1.5 <= (l_l / width) <= 2.4:
yield PASS, "Distance between vertical strokes was adequate"
else:
yield FAIL, Message(
"bad-vertical-vertical-spacing",
f"The space between vertical strokes ({l_l})"
f" does not conform to the expected"
f" range of {width * 1.5}-{width * 2.4}",
)
# For v, however, a simple LSB/RSB is adequate.
glyphset = ttFont.getGlyphSet()
pen = BoundsPen(glyphset)
glyphset["v"].draw(pen)
(xMin, yMin, xMax, yMax) = pen.bounds
v_advance = ttFont["hmtx"]["v"][0]
v_lsb = xMin
v_rsb = v_advance - (v_lsb + xMax - xMin)
l_v = l_rsb + pair_kerning(font, "l", "v") + v_lsb
if l_v is None:
yield FAIL, Message(
"glyph-not-present",
"There was no 'v' glyph in the font, so the spacing could not be tested",
)
return
if (l_v / width) > 0.85:
yield PASS, "Distance between vertical and diagonal strokes was adequate"
else:
yield FAIL, Message(
"bad-vertical-diagonal-spacing",
f"The space between vertical and diagonal strokes ({l_v})"
f" was less than the expected"
f" value of {width * 0.85}",
)
v_v = v_rsb + pair_kerning(font, "v", "v") + v_lsb
if v_v > 0:
yield PASS, "Distance between diagonal strokes was adequate"
else:
yield FAIL, Message(
"bad-diagonal-diagonal-spacing", "Diagonal strokes (vv) were touching"
)
[docs]@check(
id="com.google.fonts/check/iso15008_interword_spacing",
rationale="""
According to ISO 15008, fonts used for in-car displays
should not be too narrow or too wide.
To ensure legibility of this font on in-car information systems,
it is recommended that the space character should have advance width
between 250% and 300% of the space between the letters l and m.
"""
+ DISCLAIMER,
proposal=[
"https://github.com/fonttools/fontbakery/issues/1832",
"https://github.com/fonttools/fontbakery/issues/3253",
],
)
def com_google_fonts_check_iso15008_interword_spacing(font, ttFont):
"""Check if spacing between words is adequate for display use"""
l_intersections = xheight_intersections(ttFont, "l")
if len(l_intersections) < 2:
yield FAIL, Message(
"glyph-not-present",
"There was no 'l' glyph in the font, so the spacing could not be tested",
)
return
l_advance = ttFont["hmtx"]["l"][0]
l_rsb = l_advance - l_intersections[-1].point.x
glyphset = ttFont.getGlyphSet()
pen = BoundsPen(glyphset)
glyphset["m"].draw(pen)
(xMin, yMin, xMax, yMax) = pen.bounds
m_advance = ttFont["hmtx"]["m"][0]
m_lsb = xMin
m_rsb = m_advance - (m_lsb + xMax - xMin)
n_lsb = ttFont["hmtx"]["n"][1]
l_m = l_rsb + pair_kerning(font, "l", "m") + m_lsb
space_width = ttFont["hmtx"]["space"][0]
# Add spacing caused by normal sidebearings
space_width += m_rsb + n_lsb
if 2.50 <= space_width / l_m <= 3.0:
yield PASS, "Advance width of interword space was adequate"
else:
yield FAIL, Message(
"bad-interword-spacing",
f"The interword space ({space_width}) was"
f" outside the recommended range ({l_m*2.5}-{l_m*3.0})",
)
[docs]@check(
id="com.google.fonts/check/iso15008_interline_spacing",
rationale="""
According to ISO 15008, fonts used for in-car displays
should not be too narrow or too wide.
To ensure legibility of this font on in-car information systems,
it is recommended that the vertical metrics be set to a minimum
at least one stem width between the bottom of the descender
and the top of the ascender.
"""
+ DISCLAIMER,
proposal=[
"https://github.com/fonttools/fontbakery/issues/1832",
"https://github.com/fonttools/fontbakery/issues/3254",
],
)
def com_google_fonts_check_iso15008_interline_spacing(ttFont):
"""Check if spacing between lines is adequate for display use"""
glyphset = ttFont.getGlyphSet()
if "h" not in glyphset or "g" not in glyphset:
yield FAIL, Message(
"glyph-not-present",
"There was no 'g'/'h' glyph in the font,"
" so the spacing could not be tested",
)
return
pen = BoundsPen(glyphset)
glyphset["h"].draw(pen)
(_, _, _, h_yMax) = pen.bounds
pen = BoundsPen(glyphset)
glyphset["g"].draw(pen)
(_, g_yMin, _, _) = pen.bounds
linegap = (
(g_yMin - ttFont["OS/2"].sTypoDescender)
+ ttFont["OS/2"].sTypoLineGap
+ (ttFont["OS/2"].sTypoAscender - h_yMax)
)
width = stem_width(ttFont)
if width is None:
yield FAIL, Message("no-stem-width", "Could not determine stem width")
return
if linegap < width:
yield FAIL, Message(
"bad-interline-spacing",
f"The interline space {linegap} should"
f" be more than the stem width {width}",
)
return
yield PASS, "Amount of interline space was adequate"
profile.auto_register(globals())
profile.test_expected_checks(CHECKS, exclusive=True)