Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/crossglyph/cpfont/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ def resolve_style_coverage(primary_face, fallback_faces, intervals,
"all_glyphs", # [(GlyphProps, packed_bytes), ...]
"total_bitmap_size", # int
"advanceY", "ascender", "descender",
"max_ink_top", # int: tallest ink above the baseline in this style
"kern_left_classes", "kern_right_classes", "kern_matrix",
"kern_left_class_count", "kern_right_class_count",
"ligature_pairs",
Expand Down Expand Up @@ -1382,6 +1383,14 @@ def load_glyph_for_face(target_face, code_point, face_index=0):
total_bitmap_size += len(packed)
all_glyphs.append((glyph, packed))

# Tallest ink above the baseline, over every glyph that has ink. A reader
# that places a page's first line from it can keep tall marks, such as
# Vietnamese double accents, off the screen edge. Empty glyphs have width
# and height 0, so they cannot raise it; a style with no ink reports 0,
# which a reader takes as "unknown".
max_ink_top = max((g.top for g, _ in all_glyphs if g.width and g.height),
default=0)

# Get font metrics from the primary font. This keeps line metrics stable for
# the selected family even when a fallback contributes some glyphs.
load_glyph_for_face(face, ord('|'))
Expand Down Expand Up @@ -1473,6 +1482,7 @@ def load_glyph_for_face(target_face, code_point, face_index=0):
advanceY=advanceY,
ascender=ascender,
descender=descender,
max_ink_top=max_ink_top,
kern_left_classes=kern_left_classes,
kern_right_classes=kern_right_classes,
kern_matrix=kern_matrix,
Expand Down Expand Up @@ -1604,8 +1614,13 @@ def generate_cpfont_multistyle(style_fonts, size, intervals, output_path,
# Build style TOC entries
# Each entry: styleId(1) + pad(3) + intervalCount(4) + glyphCount(4) +
# advanceY(1) + ascender(2) + descender(2) + kernL(2) + kernR(2) +
# kernLCls(1) + kernRCls(1) + ligCount(1) + dataOffset(4) + reserved(4) = 32
STYLE_TOC_FORMAT = "<B3xIIBhhHHBBBI4x"
# kernLCls(1) + kernRCls(1) + ligCount(1) + dataOffset(4) +
# maxInkTop(2) + reserved(2) = 32
#
# maxInkTop takes the first half of what upstream leaves reserved, so the
# entry stays 32 bytes and the version stays 4. CrossPoint never reads
# those bytes. A firmware that does read it treats 0 as "unknown".
STYLE_TOC_FORMAT = "<B3xIIBhhHHBBBIh2x"
assert struct.calcsize(STYLE_TOC_FORMAT) == STYLE_TOC_ENTRY_SIZE

toc_data = bytearray()
Expand Down Expand Up @@ -1637,7 +1652,8 @@ def generate_cpfont_multistyle(style_fonts, size, intervals, output_path,
len(sd.kern_left_classes), len(sd.kern_right_classes),
sd.kern_left_class_count, sd.kern_right_class_count,
len(sd.ligature_pairs),
style_offsets[style_id])
style_offsets[style_id],
sd.max_ink_top)

# Write output
os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else ".", exist_ok=True)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_fontbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,36 @@ def test_style_metrics_reads_the_style_toc(tmp_path):
assert fontbuild.glyph_count(path) == 123


def test_max_ink_top_is_the_tallest_glyph_top(tmp_path):
"""Bytes 28 and 29 of each style entry hold the tallest ink above the
baseline. CrossPoint skips them; a firmware that reads them places the
first line of a page from the number, and takes a 0 to mean "unknown".
A zero left there is what the format used to write, so it is checked
against the glyph records the file itself carries rather than a constant."""
import struct

from fontsmith import box_font

from crossglyph import cpfont

face = box_font(tmp_path / "Probe-Regular.ttf", [0x20, 0x41, 0x61],
family="Probe")
out = tmp_path / "probe.cpfont"
cpfont.generate_cpfont_multistyle(
{0: str(face), 1: str(face)}, 12, cpfont.resolve_intervals("base"),
str(out))
blob = out.read_bytes()
for entry in (32, 64):
interval_count, glyph_count = struct.unpack_from("<II", blob, entry + 4)
offset = struct.unpack_from("<I", blob, entry + 24)[0]
glyphs = offset + interval_count * 12
tops = [struct.unpack_from("<hh", blob, glyphs + n * 16 + 4)[1]
for n in range(glyph_count)
if struct.unpack_from("<BB", blob, glyphs + n * 16) != (0, 0)]
assert max(tops) > 0
assert struct.unpack_from("<h", blob, entry + 28)[0] == max(tops)


def test_style_metrics_of_a_non_cpfont_is_zeroed(tmp_path):
path = tmp_path / "nope.cpfont"
path.write_bytes(b"not a font")
Expand Down