diff --git a/docs/font-metadata.md b/docs/font-metadata.md index 9fb9b65..8b59c38 100644 --- a/docs/font-metadata.md +++ b/docs/font-metadata.md @@ -4,6 +4,40 @@ Each font can optionally provide a metadata file containing information that can All positions and widths are specified in ems. +## metrics + +This section contains font-wide measurements and adjustments. The +`elafronBounds` object gives the horizontal outline bounds of the elafron +component in composite glyphs. + +```js +{ + "metrics": { + "ascent": 0.8, + "descent": 0.2, + "winAscent": 1.4, + "winDescent": 0.466, + "oligonMidpoint": 0.307, + "elafronBounds": { + "runningElafron": { + "left": 0.59, + "right": 1.696 + }, + "petastiRunningElafron": { + "left": 0.425, + "right": 1.431 + } + } + }, + ... +} +``` + +These bounds can be used to terminate a melisma at the component's edge +without assuming that it matches the stand-alone glyph. Given the composite +glyph's advance width, the signed offset needed to center lyrics beneath its +elafron is `left + right - advanceWidth`. + ## glyphsWithAnchors This section contains the anchor points for all glyphs that contain anchors. If mark-to-base positioning is not available, marks can be positioned using the data in this section. diff --git a/fonts/neanes.metadata.json b/fonts/neanes.metadata.json index 2497cbf..9e72d92 100644 --- a/fonts/neanes.metadata.json +++ b/fonts/neanes.metadata.json @@ -6,7 +6,17 @@ "descent": 0.2, "winAscent": 1.4, "winDescent": 0.466, - "oligonMidpoint": 0.307 + "oligonMidpoint": 0.307, + "elafronBounds": { + "runningElafron": { + "left": 0.59, + "right": 1.696 + }, + "petastiRunningElafron": { + "left": 0.425, + "right": 1.431 + } + } }, "glyphsWithAnchors": { "ison": { diff --git a/fonts/neanesengraving.metadata.json b/fonts/neanesengraving.metadata.json index 21e851f..5490ef6 100644 --- a/fonts/neanesengraving.metadata.json +++ b/fonts/neanesengraving.metadata.json @@ -6,7 +6,17 @@ "descent": 0.2, "winAscent": 1.4, "winDescent": 0.466, - "oligonMidpoint": 0.307 + "oligonMidpoint": 0.307, + "elafronBounds": { + "runningElafron": { + "left": 0.542, + "right": 1.648 + }, + "petastiRunningElafron": { + "left": 0.502, + "right": 1.508 + } + } }, "glyphsWithAnchors": { "ison": { diff --git a/fonts/neanesrtl.metadata.json b/fonts/neanesrtl.metadata.json index 9e2dcdf..bc8d655 100644 --- a/fonts/neanesrtl.metadata.json +++ b/fonts/neanesrtl.metadata.json @@ -6,7 +6,17 @@ "descent": 0.2, "winAscent": 1.4, "winDescent": 0.466, - "oligonMidpoint": 0.307 + "oligonMidpoint": 0.307, + "elafronBounds": { + "runningElafron": { + "left": 0.08, + "right": 1.185 + }, + "petastiRunningElafron": { + "left": 0.085, + "right": 1.091 + } + } }, "glyphsWithAnchors": { "ison": { diff --git a/fonts/neanesrtlengraving.metadata.json b/fonts/neanesrtlengraving.metadata.json index 96ce990..972e53d 100644 --- a/fonts/neanesrtlengraving.metadata.json +++ b/fonts/neanesrtlengraving.metadata.json @@ -6,7 +6,17 @@ "descent": 0.2, "winAscent": 1.4, "winDescent": 0.466, - "oligonMidpoint": 0.307 + "oligonMidpoint": 0.307, + "elafronBounds": { + "runningElafron": { + "left": -0.002, + "right": 1.104 + }, + "petastiRunningElafron": { + "left": 0.019, + "right": 1.024 + } + } }, "glyphsWithAnchors": { "ison": { diff --git a/fonts/neanesstathisseries.metadata.json b/fonts/neanesstathisseries.metadata.json index 3fab4af..32cdccc 100644 --- a/fonts/neanesstathisseries.metadata.json +++ b/fonts/neanesstathisseries.metadata.json @@ -6,7 +6,17 @@ "descent": 0.2, "winAscent": 1.4, "winDescent": 0.466, - "oligonMidpoint": 0.309 + "oligonMidpoint": 0.309, + "elafronBounds": { + "runningElafron": { + "left": 0.72, + "right": 1.772 + }, + "petastiRunningElafron": { + "left": 0.389, + "right": 1.515 + } + } }, "glyphsWithAnchors": { "ison": { diff --git a/fonts/neanesstathisseriesengraving.metadata.json b/fonts/neanesstathisseriesengraving.metadata.json index 2701d97..fa2eece 100644 --- a/fonts/neanesstathisseriesengraving.metadata.json +++ b/fonts/neanesstathisseriesengraving.metadata.json @@ -6,7 +6,17 @@ "descent": 0.2, "winAscent": 1.4, "winDescent": 0.466, - "oligonMidpoint": 0.309 + "oligonMidpoint": 0.309, + "elafronBounds": { + "runningElafron": { + "left": 0.72, + "right": 1.772 + }, + "petastiRunningElafron": { + "left": 0.721, + "right": 1.847 + } + } }, "glyphsWithAnchors": { "ison": { diff --git a/scripts/generate-font-metadata.py b/scripts/generate-font-metadata.py index e5e782a..db47a1c 100644 --- a/scripts/generate-font-metadata.py +++ b/scripts/generate-font-metadata.py @@ -88,6 +88,24 @@ def contour_bbox(contour): return xmin, ymin, xmax, ymax +def find_widest_contour_bbox(glyph): + bboxes = [bbox for contour in glyph.foreground if (bbox := contour_bbox(contour))] + + if not bboxes: + return None + + return max(bboxes, key=lambda bbox: bbox[2] - bbox[0]) + + +def find_lowest_contour_bbox(glyph): + bboxes = [bbox for contour in glyph.foreground if (bbox := contour_bbox(contour))] + + if not bboxes: + return None + + return min(bboxes, key=lambda bbox: bbox[1]) + + def create_temp_glyph_from_contours(font, source_glyph, contours, name): temp_glyph = font.createChar(-1, name) temp_glyph.clear() @@ -355,6 +373,7 @@ def asdict(self): "winAscent": round(self.font.os2_winascent / self.font.em, 3), "winDescent": round(self.font.os2_windescent / self.font.em, 3), "oligonMidpoint": round(self.font.oligon_midpoint / self.font.em, 3), + "elafronBounds": self.elafron_bounds(), } anchors = self.anchors() @@ -396,6 +415,32 @@ def asdict(self): return d + def elafron_bounds(self): + # In runningElafron the wider of the two adjacent contours is the + # elafron. In petastiRunningElafron the elafron is the contour below + # the petasti. Derive the component bounds from those outlines instead + # of assuming they match the stand-alone elafron. + contour_bboxes = { + "runningElafron": find_widest_contour_bbox(self.font["runningElafron"]), + "petastiRunningElafron": find_lowest_contour_bbox( + self.font["petastiRunningElafron"] + ), + } + + bounds = {} + + for glyph_name, bbox in contour_bboxes.items(): + if bbox is None: + raise ValueError(f"Cannot locate the elafron in {glyph_name}") + + elafron_left, _, elafron_right, _ = bbox + bounds[glyph_name] = { + "left": round(elafron_left / self.font.em, 3), + "right": round(elafron_right / self.font.em, 3), + } + + return bounds + def anchors(self): all_anchors = {}