Source code for neoscore.core.music_font

from __future__ import annotations

import copy
from typing import Dict, Optional, Type, Union

from neoscore.core import neoscore, smufl
from neoscore.core.exceptions import (
    MusicFontGlyphNotFoundError,
    MusicFontMetadataNotFoundError,
)
from neoscore.core.font import Font
from neoscore.core.glyph_info import GlyphInfo
from neoscore.core.point import Point
from neoscore.core.rect import Rect
from neoscore.core.units import Mm, Unit, convert_all_to_unit, make_unit_class


[docs]class MusicFont(Font): """A SMuFL compliant music font"""
[docs] def __init__(self, family_name: str, size: Union[Unit, Type[Unit]]): """ Args: family_name: The font name size: The font size, given either as the distance between two staff lines, or a unit type where ``unit(1)`` is that distance. """ if isinstance(size, Unit): self._unit = make_unit_class("StaffUnit", size.base_value) else: self._unit = size try: self.metadata = neoscore.registered_music_fonts[family_name] except KeyError: raise MusicFontMetadataNotFoundError self._engraving_defaults = copy.deepcopy(self.metadata["engravingDefaults"]) # 1 SMuFL em is the height of a 5-line staff. See: # w3c.github.io/smufl/latest/specification/scoring-metrics-glyph-registration.html self._em_size = self.unit(4) self._glyph_info_cache = {} # engraving_defaults is small, so eagerly converting it to self.unit is ok convert_all_to_unit(self._engraving_defaults, self.unit) super().__init__(family_name, self._em_size, 1, False)
def __str__(self): unit_val_as_mm = Mm(self.unit(1)).display_value return f"MusicFont('{self.family_name}', <unit(1) = Mm({unit_val_as_mm})>)" @property def unit(self) -> Type[Unit]: """A unit type where ``unit(1)`` is a standard staff space in the font.""" return self._unit @property def engraving_defaults(self) -> Dict: """The SMuFL engraving defaults for this font. See `SMuFL's description of this data here <https://w3c.github.io/smufl/latest/specification/engravingdefaults.html>`_. """ return self._engraving_defaults
[docs] def modified( # noqa self, family_name: Optional[str] = None, unit: Optional[Type[Unit]] = None ) -> MusicFont: return MusicFont( family_name if family_name is not None else self.family_name, unit if unit is not None else self.unit, )
[docs] def glyph_info( self, glyph_name: str, alternate_number: Optional[int] = None ) -> GlyphInfo: """Look up metadata on a glyph in this font. Args: glyph_name: The canonical name of the glyph, or its main version if using an alternate number. alternate_number: A glyph alternate number. Raises: MusicFontGlyphNotFoundError: If the requested glyph could not be found in the font. """ key = (glyph_name, alternate_number) cached_result = self._glyph_info_cache.get(key, None) if cached_result: return cached_result try: computed_result = self._glyph_info(glyph_name, alternate_number) except ValueError: raise MusicFontGlyphNotFoundError(glyph_name, alternate_number) self._glyph_info_cache[key] = computed_result return computed_result
def _glyph_info( self, glyph_name: str, alternate_number: Optional[int] = None ) -> GlyphInfo: # if an alt glyph get name if alternate_number: glyph_name = self._check_alternate_names(glyph_name, alternate_number) # check if glyphname exists then get details from smufl check_name = smufl.glyph_names.get(glyph_name) if check_name: codepoint = check_name["codepoint"] description = check_name["description"] else: # check is it ligature or optional glyph and get info (codepoint, description) = self._check_optional_glyphs(glyph_name) # if we have made it this far and populated # info with all other valid details advance_width = self.unit(0) glyphAdvanceWidths = self.metadata.get("glyphAdvanceWidths") if glyphAdvanceWidths: raw_advance_width = glyphAdvanceWidths.get(glyph_name) if raw_advance_width: advance_width = self.unit(raw_advance_width) bounding_rect = copy.deepcopy(self.metadata["glyphBBoxes"].get(glyph_name)) if bounding_rect: convert_all_to_unit(bounding_rect, self.unit) bounding_rect = self._convert_bbox_to_rect(bounding_rect) # get optional anchor metadata if available anchors = self._load_glyph_anchors(glyph_name) return GlyphInfo( glyph_name, codepoint, description, bounding_rect, advance_width, anchors ) @staticmethod def _convert_bbox_to_rect(b_box_dict: dict) -> Rect: """Converst the SMuFL bounding box info into a Rect class format""" # get SMuFL bbox coords ne_x = b_box_dict["bBoxNE"][0] ne_y = b_box_dict["bBoxNE"][1] sw_x = b_box_dict["bBoxSW"][0] sw_y = b_box_dict["bBoxSW"][1] # calculate neoscore Rect coords x = sw_x y = ne_y * -1 width = ne_x - sw_x height = ne_y - sw_y return Rect(x=x, y=y, width=width, height=height) def _check_alternate_names(self, glyph_name: str, alternate_number: int) -> str: """Check to see if the alternate glyph exists, if it does it then returns that glyph name. Args: glyph_name: The canonical name of the glyph, or its main version if using an alternate number. alternate_number: A glyph alternate number """ # check if glyphname has alternates alternate_glyphs = self.metadata["glyphsWithAlternates"].get(glyph_name) # Alternate not found in the font if not alternate_glyphs: raise ValueError else: # check if valid alt number alt_count = len(alternate_glyphs["alternates"]) # if the alternate_number is in range if alt_count >= alternate_number: new_glyph_name = alternate_glyphs["alternates"][alternate_number - 1][ "name" ] else: # Alternate number out of range raise ValueError return new_glyph_name def _check_optional_glyphs(self, glyph_name: str) -> Tuple[str, str]: """Check to see if the called glyph exists as an optional, if it does it then populate the GlyphInfo dataclass with basic info. Args: info: a partially populated GlyphInfo dataclass glyph_name: The canonical name of the glyph, or its main version if using an alternate number. """ # else check the optional glyph list optional_glyph_field = self.metadata["optionalGlyphs"].get(glyph_name) if optional_glyph_field: codepoint = optional_glyph_field["codepoint"] # some don't have descriptions description = optional_glyph_field.get("description") # else glyphname is not registered with SMuFL else: raise ValueError return codepoint, description def _load_glyph_anchors(self, glyph_name: str) -> Optional[Dict[str, Point]]: """Load any glyph anchors and convert coordinates to neoscore points.""" anchors = self.metadata["glyphsWithAnchors"].get(glyph_name) if anchors is None: return None anchors = copy.deepcopy(anchors) for key, value in anchors.items(): # SMuFL coords have opposite Y axis as neoscore, so flip # when wrapping in Point and Unit. anchors[key] = Point(self.unit(value[0]), self.unit(-value[1])) return anchors