Source code for maelzel.scoring.renderoptions

from __future__ import annotations
from dataclasses import dataclass, replace as _dataclassreplace, fields as _dataclassfields
from maelzel.scoring import enharmonics
from maelzel.textstyle import TextStyle
import emlib.misc


[docs] @dataclass(unsafe_hash=True) class RenderOptions: """ Holds all options needed for rendering """ orientation: str = 'portrait' """One of portrait, landscape""" staffSize: int | float = 12.0 """The size of each staff, in points""" pageSize: str = 'a4' """The page size, on of a1, a2, a3, a4, a5""" pageMarginMillimeters: int = 4 """Page margin in mm""" divsPerSemitone: int = 4 """Number of divisions of the semitone""" showCents: bool = False """Show cents deviation as a text annotation""" centsAnnotationPlacement: str = "above" """Placement of the cents text annotation (above | below)""" centsAnnotationFontsize: int | float = 10 """Fontsize of the cents text annotation""" centsAnnotationSeparator: str = ',' """Separator for cents annotations to be used in chords""" centsTextSnap: int = 2 """No cents annotation is added to a pitch if it is within this number of cents from the nearest microtone accoring to divsPerSemitone""" centsTextPlusSign: bool = False """Show a plus sign for possitive cents annotations""" noteLabelStyle: str = 'fontsize=10' """Style applied to labels""" glissAllowNonContiguous: bool = False """Allow non contiguous gliss""" glissHideTiedNotes: bool = True """Hide the notehead of intermediate notes within a glissando""" horizontalSpace: str = 'large' """The horizontal spacing (large | medium | small). Only used by lilypond backend""" pngResolution: int = 200 """DPI resolution of generated png images""" removeRedundantDynamics: bool = True """If True, remove superfluous dynamics""" dynamicsResetTime: int = 0 """Reset dynamics after this number of quarternotes""" dynamicsResetAfterEmptyMeasure: bool = True """Reset dynamics after empty measures""" dynamicsResetAfterRest: int = 1 """Reset dynamics after a rest of duration greater than this value""" respellPitches: bool = True """Find the best enharmonic representation""" glissLineThickness: int = 1 """Thickness of the glissando line""" glissLineType: str = 'solid' """Line type used in a glissando (solid | wavy)""" renderFormat: str = '' """The format renderer (pdf | png)""" cropToContent: bool | None = None """Crop the rendered image to the contexts""" preview: bool = False """Only render a preview (only the first system)""" opaque: bool = True """Remove any transparency in the background (when rendering to png)""" articulationInsideTie: bool = True """Render articulations even if the note is tied""" dynamicInsideTie: bool = True """Render dynamics if the note is tied""" rehearsalMarkStyle: str = 'box=square; bold; fontsize=13' """Text style used for rehearsal marks""" measureLabelStyle: str = 'box=rectangle; fontsize=12' """Style used for measure annotations""" enharmonicGroupSize: int = 6 """How much horizontal context to take into consideration when finding the best enharmonic spelling""" enharmonicStep: int = 3 """The step size of the window when evaluating the best enharmonic spelling""" enharmonicDebug: bool = False """If True, show debug information regarding the enharmonic spelling process""" enharmonicHorizontalWeight: float = 1. """The weight of the horizontal dimension during the enharmonic spelling process""" enharmonicVerticalWeight: float = 0.05 """The weight of the vertical dimension during the enharmonic spelling process""" enharmonicThreeQuarterMicrotonePenalty: float = 100 """Penalty for spelling a note using three quarter microtones""" backend: str = 'lilypond' """Backend used for rendering (lilypond | musicxml)""" title: str = '' """The title of this score""" composer: str = '' """The composer of this score""" # Options only relevant for lilypond render lilypondPngStaffsizeScale: float = 1.4 """Png staffsize scaling when rendering to lilypond""" lilypondGlissMinLength: int = 5 """Mininum length of the glissando line when rendering with lilypond. This is to avoid too short glissandi actually not showing at all""" lilypondBinary: str = '' """The lilypond binary used""" musescoreBinary: str = '' """The musescore binary""" musicxmlSolidSlideGliss: bool = True """If True, use a solid slide line for glissando in musicxml""" musicxmlIndent: str = ' ' """The indentation used when rendering the xml in musicxml""" musicxmlFontScaling: float = 1.0 """A scaling factor applied to fontsize when rendering to musicxml""" referenceStaffsize: float = 12.0 """The reference staff size. This is used to convert staffsize to a scaling factor""" musicxmlTenths: int = 40 """Tenths used when rendering to musicxml. This is a reference value""" autoClefChanges: bool = False """If True, add clef changes if necessary along a part during the rendering process""" autoClefChangesWindow: int = 1 """When adding automatic clef changes, use this window size (number of elements per evaluation)""" keepClefBiasFactor: float = 2.0 """The higher this value, the more priority is given to keeping the previous clef""" clefSimplifyThreshold: float = 0. """Threshold used to simplify automatic clef changes""" compoundMeterSubdivision: str = 'all' """Sets the subdivision policy for compound meters. One of 'all', 'none', 'heterogeneous' * 'all': add subdivisions to all internal subdivisions. * 'none': do not add any subdivision, let the backend decide * 'heterogeneous': add only subdivisions for compound meters with multiple denominators, like 3/4+3/8 """ addSubdivisionsForSmallDenominators: bool = True """ Add subdivisions for measures with a time signature with a small denominator A small denominator depends on tempo """ proportionalSpacing: bool = False """Use proportional spacing""" proportionalNotationDuration: str = '1/20' """A lower value results in a note taking more horizontal space""" proportionalSpacingKind: str = 'strict' flagStyle: str = 'normal' """Flag style, one of 'normal', 'straight', 'flat'""" useStemlets: bool = True """If the backend allows, extend stems over rests when beaming over rests""" stemletLength: float = 0.75 """Stemlet length when rendering, as a fraction of the normal stem length"""
[docs] @classmethod def keys(cls) -> set[str]: return {f.name for f in _dataclassfields(cls)}
#def __hash__(self) -> int: # return hash(str(self)) def __eq__(self, other: RenderOptions) -> bool: return isinstance(other, RenderOptions) and hash(self) == hash(other) def __post_init__(self): self.pageSize = self.pageSize.lower() self.check()
[docs] def musicxmlTenthsScaling(self) -> tuple[float, int]: """ Maps mm to tenths when rendering musicxml The scaling is based on the staff size. This allows to use one setting for all backends """ scaling = self.staffSize / 12. mm = 6.35 * scaling return (mm, self.musicxmlTenths)
[docs] def copy(self) -> RenderOptions: """ Copy this object Returns: a copy of this RenderOptions """ return _dataclassreplace(self)
[docs] def clone(self, **changes) -> RenderOptions: """ Clone this RenderOptions with the given changes Args: **changes: any attribute accepted by this RenderOptions Returns: a new RenderOptions with the changes applied Example ~~~~~~~ >>> defaultoptions = RenderOptions() >>> modified = defaultoptions.clone(pageSize='A3') """ out = _dataclassreplace(self, **changes) out.check() return out
[docs] def check(self) -> None: """ Check that the options are valid raises ValueError if an error is found """ def checkChoice(key, choices): value = getattr(self, key) if value not in choices: if isinstance(value, str): value = f"'{value}'" raise ValueError(f'Invalid {key}, it should be one of {choices}, got {value}') checkChoice('orientation', ('portrait', 'landscape')) checkChoice('pageSize', ('a1', 'a2', 'a3', 'a4', 'a5')) checkChoice('divsPerSemitone', (1, 2, 4)) checkChoice('centsAnnotationPlacement', ('above', 'below')) checkChoice('horizontalSpace', ('small', 'medium', 'large', 'xlarge', 'default')) checkChoice('backend', ('lilypond', 'musicxml')) if not (isinstance(self.staffSize, (int, float)) and 2 < self.staffSize < 40): raise ValueError(f"Invalid staffSize: {self.staffSize}") heightmm, widthmm = emlib.misc.page_dinsize_to_mm(self.pageSize, self.orientation) if not (isinstance(self.pageMarginMillimeters, int) and 0 <= self.pageMarginMillimeters <= widthmm): raise ValueError(f"Invalid value for pageMarginMillimeters, it should be an int between 0 and {widthmm}, " f"got {self.pageMarginMillimeters}")
[docs] def pageSizeMillimeters(self) -> tuple[float, float]: """ Returns the page size in millimeters Returns: a tuple (height, width), where both height and width are measured in millimeters """ return emlib.misc.page_dinsize_to_mm(self.pageSize, self.orientation)
[docs] @staticmethod def parseTextStyle(style: str) -> TextStyle: """ Parses a textstyle (measureLabelStyle, rehearsalMarkStyle, ...) Args: style: the style to parse Returns: a TextStyle """ return TextStyle.parse(style)
@property def parsedRehearsalMarkStyle(self) -> TextStyle: """The style for rehearsal marks, parsed Returns: a :class:`maelzel.scoring.textstyle.TextStyle` """ return TextStyle.parse(self.rehearsalMarkStyle) @property def parsedmeasureLabelStyle(self) -> TextStyle: """ Parses the measure annotation style Returns: a TextStyle """ return TextStyle.parse(self.measureLabelStyle)
[docs] def makeEnharmonicOptions(self) -> enharmonics.EnharmonicOptions: """ Create enharmonic options from this RenderOptions Returns: an EnharmonicOptions object derived from this RenderOptions """ return enharmonics.EnharmonicOptions(groupSize=self.enharmonicGroupSize, groupStep=self.enharmonicStep, debug=self.enharmonicDebug, threeQuarterMicrotonePenalty=self.enharmonicThreeQuarterMicrotonePenalty, horizontalWeight=self.enharmonicHorizontalWeight, verticalWeight=self.enharmonicVerticalWeight)