Exemplo n.º 1
0
def com_google_fonts_check_monospace_max_advancewidth(ttFont,
                                                      glyph_metrics_stats):
    """Monospace font has hhea.advanceWidthMax equal to each glyph's
  advanceWidth?"""
    from fontbakery.utils import pretty_print_list

    seems_monospaced = glyph_metrics_stats["seems_monospaced"]
    if not seems_monospaced:
        yield SKIP, ("Font is not monospaced.")
        return

    # hhea:advanceWidthMax is treated as source of truth here.
    max_advw = ttFont['hhea'].advanceWidthMax
    outliers = []
    zero_or_double_width_outliers = []
    glyphSet = ttFont.getGlyphSet().keys(
    )  # TODO: remove .keys() when fonttools is updated to 3.27
    glyphs = [g for g in glyphSet if g not in ['.notdef', '.null', 'NULL']]
    for glyph_id in glyphs:
        width = ttFont['hmtx'].metrics[glyph_id][0]
        if width != max_advw:
            outliers.append(glyph_id)
        if width == 0 or width == 2 * max_advw:
            zero_or_double_width_outliers.append(glyph_id)

    if outliers:
        outliers_percentage = float(len(outliers)) / len(glyphSet)
        yield WARN,\
              Message("should-be-monospaced",
                      "This seems to be a monospaced font,"
                      " so advanceWidth value should be the same"
                      " across all glyphs, but {}% of them"
                      " have a different value: {}"
                      "".format(round(100 * outliers_percentage, 2),
                                pretty_print_list(outliers)))
        if zero_or_double_width_outliers:
            yield WARN,\
                  Message("variable-monospaced",
                          "Double-width and/or zero-width glyphs"
                          " were detected. These glyphs should be set"
                          " to the same width as all others"
                          " and then add GPOS single pos lookups"
                          " that zeros/doubles the widths as needed:"
                          " {}".format(pretty_print_list(
                                       zero_or_double_width_outliers)))
    else:
        yield PASS, ("hhea.advanceWidthMax is equal"
                     " to all glyphs' advanceWidth in this monospaced font.")
Exemplo n.º 2
0
def com_google_fonts_check_valid_glyphnames(ttFont):
    """Glyph names are all valid?"""
    from fontbakery.utils import pretty_print_list

    if ttFont.sfntVersion == b'\x00\x01\x00\x00' and ttFont.get(
            "post") and ttFont["post"].formatType == 3.0:
        yield SKIP, ("TrueType fonts with a format 3.0 post table contain no"
                     " glyph names.")
    else:
        import re
        bad_names = []
        warn_names = []
        for _, glyphName in enumerate(ttFont.getGlyphOrder()):
            if glyphName in [".null", ".notdef", ".ttfautohint"]:
                # These 2 names are explicit exceptions
                # in the glyph naming rules
                continue
            if not re.match(r'^(?![.0-9])[a-zA-Z._0-9]{1,63}$', glyphName):
                bad_names.append(glyphName)
            if len(glyphName) > 31 and len(glyphName) <= 63:
                warn_names.append(glyphName)

        if len(bad_names) == 0:
            if len(warn_names) == 0:
                yield PASS, "Glyph names are all valid."
            else:
                yield WARN,\
                      Message('legacy-long-names',
                              ("The following glyph names may be too"
                               " long for some legacy systems which may"
                               " expect a maximum 31-char length limit:\n"
                               "{}").format(pretty_print_list(warn_names)))
        else:
            yield FAIL,\
                  Message('found-invalid-names',
                          ("The following glyph names do not comply"
                           " with naming conventions: {}\n\n"
                           " A glyph name"
                           " must be entirely comprised of characters from"
                           " the following set:"
                           " A-Z a-z 0-9 .(period) _(underscore)."
                           " A glyph name must not start with a digit or period."
                           " There are a few exceptions"
                           " such as the special glyph \".notdef\"."
                           " The glyph names \"twocents\", \"a1\", and \"_\""
                           " are all valid, while \"2cents\""
                           " and \".twocents\" are not."
                           "").format(pretty_print_list(bad_names)))
Exemplo n.º 3
0
def com_google_fonts_check_glyf_non_transformed_duplicate_components(ttFont):
    """Check glyphs do not have duplicate components which have the same x,y coordinates."""
    from fontbakery.utils import pretty_print_list
    failed = []
    for glyph_name in ttFont['glyf'].keys():
        glyph = ttFont['glyf'][glyph_name]
        if not glyph.isComposite():
            continue

        seen = []
        for comp in glyph.components:
            comp_info = {
                "glyph": glyph_name,
                "component": comp.glyphName,
                "x": comp.x,
                "y": comp.y
            }
            if comp_info in seen:
                failed.append(comp_info)
            else:
                seen.append(comp_info)
    if failed:
        formatted_list = "\t* " + pretty_print_list(failed,
                                                    shorten=10,
                                                    sep="\n\t* ")
        yield FAIL, \
              Message('found-duplicates',
                      f"The following glyphs have duplicate components which"
                      f" have the same x,y coordinates:\n"
                      f"{formatted_list}")
    else:
        yield PASS, ("Glyphs do not contain duplicate components which have"
                     " the same x,y coordinates.")
Exemplo n.º 4
0
def com_google_fonts_check_points_out_of_bounds(ttFont):
    """Check for points out of bounds."""
    from fontbakery.utils import pretty_print_list
    passed = True
    out_of_bounds = []
    for glyphName in ttFont['glyf'].keys():
        glyph = ttFont['glyf'][glyphName]
        coords = glyph.getCoordinates(ttFont['glyf'])[0]
        for x, y in coords:
            if round(x) < glyph.xMin or round(x) > glyph.xMax or \
               round(y) < glyph.yMin or round(y) > glyph.yMax or \
               abs(x) > 32766 or abs(y) > 32766:
                passed = False
                out_of_bounds.append((glyphName, x, y))

    if not passed:
        formatted_list = "\t* " + pretty_print_list(out_of_bounds,
                                                    shorten=10,
                                                    sep="\n\t* ")
        yield WARN,\
              Message("points-out-of-bounds",
                      f"The following glyphs have coordinates"
                      f" which are out of bounds:\n"
                      f"{formatted_list}\n"
                      f"\n"
                      f"This happens a lot when points are not extremes,"
                      f" which is usually bad. However, fixing this alert"
                      f" by adding points on extremes may do more harm"
                      f" than good, especially with italics,"
                      f" calligraphic-script, handwriting, rounded and"
                      f" other fonts. So it is common to ignore this message.")
    else:
        yield PASS, "All glyph paths have coordinates within bounds!"
Exemplo n.º 5
0
def com_google_fonts_check_gdef_mark_chars(ttFont):
    """Check mark characters are in GDEF mark glyph class"""
    from fontbakery.utils import pretty_print_list

    if "GDEF" in ttFont and ttFont["GDEF"].table.GlyphClassDef:
        cmap = ttFont.getBestCmap()
        mark_class_glyphnames = _get_mark_class_glyphnames(ttFont)
        mark_chars_not_in_mark_class = {
            charcode
            for charcode in cmap if _is_non_mark_char(charcode) is False
            and cmap[charcode] not in mark_class_glyphnames
        }

        if mark_chars_not_in_mark_class:
            formatted_marks = "\t " +\
              pretty_print_list(sorted("U+%04X" % c for c in
                                       mark_chars_not_in_mark_class),
                                shorten=None,
                                sep=", ")
            yield WARN,\
                  Message('mark-chars',
                          f"The following mark characters could be"
                          f" in the GDEF mark glyph class:\n"
                          f"{formatted_marks}")
        else:
            yield PASS, ('Font does not have mark characters'
                         ' not in the GDEF mark glyph class.')
    else:
        yield SKIP, ('Font does not declare an optional "GDEF" table'
                     ' or has any GDEF glyph class definition.')
Exemplo n.º 6
0
def com_google_fonts_check_gdef_spacing_marks(ttFont):
    """Check mark characters are in GDEF mark glyph class)"""
    from fontbakery.utils import pretty_print_list

    if "GDEF" in ttFont and ttFont["GDEF"].table.GlyphClassDef:
        spacing_glyphnames = {
            name
            for (name, (width, lsb)) in ttFont["hmtx"].metrics.items()
            if width > 0
        }
        mark_class_glyphnames = _get_mark_class_glyphnames(ttFont)
        spacing_glyphnames_in_mark_glyph_class = spacing_glyphnames & mark_class_glyphnames
        if spacing_glyphnames_in_mark_glyph_class:
            formatted_list = "\t " +\
              pretty_print_list(sorted(spacing_glyphnames_in_mark_glyph_class),
                                shorten=10,
                                sep=", ")
            yield WARN,\
                  Message('spacing-mark-glyphs',
                          f"The following spacing glyphs may be in"
                          f" the GDEF mark glyph class by mistake:\n"
                          f"{formatted_list}")
        else:
            yield PASS, ('Font does not has spacing glyphs'
                         ' in the GDEF mark glyph class.')
    else:
        yield SKIP, ('Font does not declare an optional "GDEF" table'
                     ' or has any GDEF glyph class definition.')
Exemplo n.º 7
0
def com_google_fonts_check_valid_glyphnames(ttFont):
  """Glyph names are all valid?"""
  if ttFont.sfntVersion == b'\x00\x01\x00\x00' and ttFont.get(
      "post") and ttFont["post"].formatType == 3.0:
    yield SKIP, ("TrueType fonts with a format 3.0 post table contain no"
                 " glyph names.")
  else:
    import re
    bad_names = []
    for _, glyphName in enumerate(ttFont.getGlyphOrder()):
      if glyphName in [".null", ".notdef", ".ttfautohint"]:
        # These 2 names are explicit exceptions
        # in the glyph naming rules
        continue
      if not re.match(r'^(?![.0-9])[a-zA-Z._0-9]{1,31}$', glyphName):
        bad_names.append(glyphName)

    if len(bad_names) == 0:
      yield PASS, "Glyph names are all valid."
    else:
      from fontbakery.utils import pretty_print_list
      yield FAIL, Message("found-invalid-names",
                  ("The following glyph names do not comply"
                   " with naming conventions: {}\n\n"
                   " A glyph name may be up to 31 characters in length,"
                   " must be entirely comprised of characters from"
                   " the following set:"
                   " A-Z a-z 0-9 .(period) _(underscore). and must not"
                   " start with a digit or period."
                   " There are a few exceptions"
                   " such as the special character \".notdef\"."
                   " The glyph names \"twocents\", \"a1\", and \"_\""
                   " are all valid, while \"2cents\""
                   " and \".twocents\" are not."
                   "").format(pretty_print_list(bad_names)))
Exemplo n.º 8
0
def com_google_fonts_check_outline_short_segments(ttFont, outlines_dict):
    """Are any segments inordinately short?"""
    warnings = []
    for glyphname, outlines in outlines_dict.items():
        for p in outlines:
            outline_length = p.length
            segments = p.asSegments()
            if not segments:
                continue
            prev_was_line = len(segments[-1]) == 2
            for seg in p.asSegments():
                if math.isclose(seg.length, 0):  # That's definitely wrong
                    warnings.append(f"{glyphname} contains a short segment {seg}")
                elif (
                    seg.length < SHORT_PATH_ABSOLUTE_EPSILON
                    or seg.length < SHORT_PATH_EPSILON * outline_length
                ) and (prev_was_line or len(seg) > 2):
                    warnings.append(f"{glyphname} contains a short segment {seg}")
                prev_was_line = len(seg) == 2
        if len(warnings) > FALSE_POSITIVE_CUTOFF:
            yield PASS, ("So many short segments were found"
                         " that this was probably by design.")
            return

    if warnings:
        formatted_list = "\t* " + pretty_print_list(warnings, sep="\n\t* ")
        yield WARN,\
              Message("found-short-segments",
                      f"The following glyphs have segments which seem very short:\n"
                      f"{formatted_list}")
    else:
        yield PASS, "No short segments were found."
Exemplo n.º 9
0
def com_google_fonts_check_outline_jaggy_segments(ttFont, outlines_dict):
    """Do outlines contain any jaggy segments?"""
    warnings = []
    for glyphname, outlines in outlines_dict.items():
        for p in outlines:
            segments = p.asSegments()
            if not segments:
                continue
            for i in range(0, len(segments)):
                prev = segments[i - 1]
                this = segments[i]
                in_vector = prev.tangentAtTime(1) * -1
                out_vector = this.tangentAtTime(0)
                if not (in_vector.magnitude * out_vector.magnitude):
                    continue
                angle = (in_vector @ out_vector) / (
                    in_vector.magnitude * out_vector.magnitude
                )
                if not (-1 <= angle <= 1):
                    continue
                jag_angle = math.acos(angle)
                if abs(jag_angle) > JAG_ANGLE or jag_angle == 0:
                    continue
                warnings.append(f"{glyphname}:"
                                f" {prev}/{this} = {math.degrees(jag_angle)}")

    if warnings:
        formatted_list = "\t* " + pretty_print_list(sorted(warnings), sep="\n\t* ")
        yield WARN,\
              Message("found-jaggy-segments",
                      f"The following glyphs have jaggy segments:\n"
                      f"{formatted_list}")
    else:
        yield PASS, "No jaggy segments found."
Exemplo n.º 10
0
def com_google_fonts_check_outline_colinear_vectors(ttFont, outlines_dict):
    """Do any segments have colinear vectors?"""
    warnings = []
    for glyphname, outlines in outlines_dict.items():
        for p in outlines:
            segments = p.asSegments()
            if not segments:
                continue
            for i in range(0, len(segments)):
                prev = segments[i - 1]
                this = segments[i]
                if len(prev) == 2 and len(this) == 2:
                    if (
                        abs(prev.tangentAtTime(0).angle - this.tangentAtTime(0).angle)
                        < COLINEAR_EPSILON
                    ):
                        warnings.append(f"{glyphname}: {prev} -> {this}")
        if len(warnings) > FALSE_POSITIVE_CUTOFF:
            yield PASS, ("So many colinear vectors were found"
                         " that this was probably by design.")
            return

    if warnings:
        formatted_list = "\t* " + pretty_print_list(sorted(set(warnings)), sep="\n\t* ")
        yield WARN,\
              Message("found-colinear-vectors",
                      f"The following glyphs have colinear vectors:\n"
                      f"{formatted_list}")
    else:
        yield PASS, "No colinear vectors found."
Exemplo n.º 11
0
def com_google_fonts_check_valid_glyphnames(ttFont):
  """Glyph names are all valid?"""
  if ttFont.sfntVersion == b'\x00\x01\x00\x00' and ttFont.get(
      "post") and ttFont["post"].formatType == 3.0:
    yield SKIP, ("TrueType fonts with a format 3.0 post table contain no"
                 " glyph names.")
  else:
    import re
    bad_names = []
    for _, glyphName in enumerate(ttFont.getGlyphOrder()):
      if glyphName in [".null", ".notdef", ".ttfautohint"]:
        # These 2 names are explicit exceptions
        # in the glyph naming rules
        continue
      if not re.match(r'^(?![.0-9])[a-zA-Z._0-9]{1,31}$', glyphName):
        bad_names.append(glyphName)

    if len(bad_names) == 0:
      yield PASS, "Glyph names are all valid."
    else:
      from fontbakery.utils import pretty_print_list
      yield FAIL, ("The following glyph names do not comply"
                   " with naming conventions: {}\n\n"
                   " A glyph name may be up to 31 characters in length,"
                   " must be entirely comprised of characters from"
                   " the following set:"
                   " A-Z a-z 0-9 .(period) _(underscore). and must not"
                   " start with a digit or period."
                   " There are a few exceptions"
                   " such as the special character \".notdef\"."
                   " The glyph names \"twocents\", \"a1\", and \"_\""
                   " are all valid, while \"2cents\""
                   " and \".twocents\" are not."
                   "").format(pretty_print_list(bad_names))
Exemplo n.º 12
0
def com_google_fonts_check_outline_semi_vertical(ttFont, outlines_dict):
    """Do outlines contain any semi-vertical or semi-horizontal lines?"""
    warnings = []
    for glyphname, outlines in outlines_dict.items():
        for p in outlines:
            segments = p.asSegments()
            if not segments:
                continue
            for s in segments:
                if len(s) != 2:
                    continue
                angle = math.degrees((s.end - s.start).angle)
                for yExpected in [-180, -90, 0, 90, 180]:
                    if close_but_not_on(angle, yExpected, 0.5):
                        warnings.append(f"{glyphname}: {s}")

    if warnings:
        formatted_list = "\t* " + pretty_print_list(sorted(warnings),
                                                    sep="\n\t* ")
        yield WARN,\
             Message("found-semi-vertical",
                     f"The following glyphs have semi-vertical/semi-horizontal lines:\n"
                     f"{formatted_list}")
    else:
        yield PASS, "No semi-horizontal/semi-vertical lines found."
Exemplo n.º 13
0
def com_google_fonts_check_monospace_max_advancewidth(ttFont, glyph_metrics_stats):
  """Monospace font has hhea.advanceWidthMax equal to each glyph's
  advanceWidth?"""
  from fontbakery.utils import pretty_print_list

  seems_monospaced = glyph_metrics_stats["seems_monospaced"]
  if not seems_monospaced:
    yield SKIP, ("Font is not monospaced.")
    return

  # hhea:advanceWidthMax is treated as source of truth here.
  max_advw = ttFont['hhea'].advanceWidthMax
  outliers = []
  zero_or_double_width_outliers = []
  glyphSet = ttFont.getGlyphSet().keys() # TODO: remove .keys() when fonttools is updated to 3.27
  glyphs = [
      g for g in glyphSet if g not in ['.notdef', '.null', 'NULL']
  ]
  for glyph_id in glyphs:
    width = ttFont['hmtx'].metrics[glyph_id][0]
    if width != max_advw:
      outliers.append(glyph_id)
    if width == 0 or width == 2 * max_advw:
      zero_or_double_width_outliers.append(glyph_id)

  if outliers:
    outliers_percentage = float(len(outliers)) / len(glyphSet)
    yield WARN, Message("should-be-monospaced",
                        "This seems to be a monospaced font,"
                        " so advanceWidth value should be the same"
                        " across all glyphs, but {}% of them"
                        " have a different value: {}"
                        "".format(round(100 * outliers_percentage, 2),
                                  pretty_print_list(outliers)))
    if zero_or_double_width_outliers:
      yield WARN, Message("variable-monospaced",
                          "Double-width and/or zero-width glyphs"
                          " were detected. These glyphs should be set"
                          " to the same width as all others"
                          " and then add GPOS single pos lookups"
                          " that zeros/doubles the widths as needed:"
                          " {}".format(pretty_print_list(
                                         zero_or_double_width_outliers)))
  else:
    yield PASS, ("hhea.advanceWidthMax is equal"
                 " to all glyphs' advanceWidth in this monospaced font.")
Exemplo n.º 14
0
def com_google_fonts_check_gdef_non_mark_chars(ttFont):
    """Check GDEF mark glyph class doesn't have characters that are not marks)"""
    from fontbakery.utils import pretty_print_list

    if "GDEF" in ttFont and ttFont["GDEF"].table.GlyphClassDef:
        cmap = ttFont.getBestCmap()
        nonmark_chars = {
            charcode
            for charcode in cmap
            if _is_non_spacing_mark_char(charcode) is False
        }
        nonmark_char_glyphnames = {cmap[c] for c in nonmark_chars}
        glyphname_to_char_mapping = dict()
        for k, v in cmap.items():
            if v in glyphname_to_char_mapping:
                glyphname_to_char_mapping[v].add(k)
            else:
                glyphname_to_char_mapping[v] = {k}
        mark_class_glyphnames = _get_mark_class_glyphnames(ttFont)
        nonmark_char_glyphnames_in_mark_class = nonmark_char_glyphnames & mark_class_glyphnames
        if nonmark_char_glyphnames_in_mark_class:
            nonmark_chars_in_mark_class = set()
            for glyphname in nonmark_char_glyphnames_in_mark_class:
                chars = glyphname_to_char_mapping[glyphname]
                for char in chars:
                    if char in nonmark_chars:
                        nonmark_chars_in_mark_class.add(char)
            formatted_nonmarks = "\t " +\
                                 pretty_print_list(sorted("U+%04X" % c for c in
                                                          nonmark_chars_in_mark_class),
                                                   shorten=None,
                                                   sep=", ")
            yield WARN,\
                  Message('non-mark-chars',
                          f"The following non-mark characters should"
                          f" not be in the GDEF mark glyph class:\n"
                          f"{formatted_nonmarks}")
        else:
            yield PASS, ('Font does not have non-mark characters'
                         ' in the GDEF mark glyph class.')
    else:
        yield SKIP, ('Font does not declare an optional "GDEF" table'
                     ' or has any GDEF glyph class definition.')
Exemplo n.º 15
0
def com_google_fonts_check_glyf_nested_components(ttFont):
    """Check glyphs do not have components which are themselves components."""
    from fontbakery.utils import pretty_print_list
    failed = []
    for glyph_name in ttFont['glyf'].keys():
        glyph = ttFont['glyf'][glyph_name]
        if not glyph.isComposite():
            continue
        for comp in glyph.components:
            if ttFont['glyf'][comp.glyphName].isComposite():
                failed.append(glyph_name)
    if failed:
        formatted_list = "\t* " + pretty_print_list(failed,
                                                    shorten=10,
                                                    sep="\n\t* ")
        yield FAIL, \
              Message('found-nested-components',
                      f"The following glyphs have components which"
                      f" themselves are component glyphs:\n"
                      f"{formatted_list}")
    else:
        yield PASS, ("Glyphs do not contain nested components.")
Exemplo n.º 16
0
def com_google_fonts_check_outline_alignment_miss(ttFont, outlines_dict):
    """Are there any misaligned on-curve points?"""
    alignments = {
        "baseline": 0,
        "x-height": ttFont["OS/2"].sxHeight,
        "cap-height": ttFont["OS/2"].sCapHeight,
        "ascender": ttFont["OS/2"].sTypoAscender,
        "descender": ttFont["OS/2"].sTypoDescender,
    }
    warnings = []
    for glyphname, outlines in outlines_dict.items():
        for p in outlines:
            for node in p.asNodelist():
                if node.type == "offcurve":
                    continue
                for line, yExpected in alignments.items():
                    # skip x-height check for caps
                    if line == "x-height" and (
                        len(glyphname) > 1 or glyphname[0].isupper()
                    ):
                        continue
                    if close_but_not_on(yExpected, node.y, ALIGNMENT_MISS_EPSILON):
                        warnings.append(f"{glyphname}: X={node.x},Y={node.y}"
                                        f" (should be at {line} {yExpected}?)")
        if len(warnings) > FALSE_POSITIVE_CUTOFF:
            # Let's not waste time.
            yield PASS, ("So many Y-coordinates of points were close to"
                         " boundaries that this was probably by design.")
            return

    if warnings:
        formatted_list = "\t* " + pretty_print_list(warnings, sep="\n\t* ")
        yield WARN,\
              Message("found-misalignments",
                      f"The following glyphs have on-curve points which"
                      f" have potentially incorrect y coordinates:\n"
                      f"{formatted_list}")
    else:
        yield PASS, "Y-coordinates of points fell on appropriate boundaries."