from typing import Union, Optional
from musicscore.util import isinstance_as_string
from musicscore.xmlwrapper import XMLWrapper
from musicxml.xmlelement.xmlelement import (
XMLPageLayout,
XMLPageMargins,
XMLScaling,
XMLDefaults,
XMLSystemLayout,
XMLSystemMargins,
XMLStaffLayout,
)
__all__ = [
"PAGE_MARGINS",
"PAGE_SIZES",
"SYSTEM_MARGINS",
"SYSTEM_LAYOUT",
"STAFF_LAYOUT",
"SCALING",
"Margins",
"Scaling",
"PageLayout",
"SystemLayout",
"StaffLayout",
]
#:
PAGE_MARGINS = {
"A4": {
"portrait": {"left": 140, "right": 70, "top": 70, "bottom": 70},
"landscape": {"left": 111, "right": 70, "top": 70, "bottom": 70},
},
"A3": {
"portrait": {"left": 111, "right": 70, "top": 70, "bottom": 70},
"landscape": {"left": 111, "right": 70, "top": 70, "bottom": 70},
},
}
#:
PAGE_SIZES = {"A4": (209.991, 297.0389), "A3": (297.0389, 419.9819)}
#:
SYSTEM_MARGINS = {"left": 0, "right": 0}
#:
SYSTEM_LAYOUT = {"system_distance": 117, "top_system_distance": 117}
#:
STAFF_LAYOUT = {"staff_distance": 80}
#:
SCALING = {"millimeters": 7.2319, "tenths": 40}
class LayoutMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._parent = None
@property
def parent(self):
"""
:return: musicscore object which uses this layout object.
.. todo::
Print implementation: At this moment only :obj:`~musicscore.score.Score` is implemented.
"""
return self._parent
@parent.setter
def parent(self, val):
self._parent = val
if isinstance(self, PageLayout):
if isinstance_as_string(self.parent, "Score"):
self._update()
self.parent.xml_object.xml_defaults.xml_page_layout = self.xml_object
else:
raise NotImplementedError
elif isinstance_as_string(self, "SystemLayout"):
if isinstance_as_string(self.parent, "Score"):
self.parent.xml_object.xml_defaults.xml_system_layout = self.xml_object
else:
raise NotImplementedError
elif isinstance_as_string(self, "StaffLayout"):
if isinstance_as_string(self.parent, "Score"):
self.parent.xml_object.xml_defaults.xml_staff_layout = self.xml_object
else:
raise NotImplementedError
else:
raise NotImplementedError
[docs]class Margins:
def __init__(
self,
parent: Union["PageLayout", "SystemLayout"],
left: Union[float, int] = None,
right: Union[float, int] = None,
top: Union[float, int] = None,
bottom: Union[float, int] = None,
):
self._parent = None
self._left = None
self._right = None
self._top = None
self._parent_xml_object = None
self._parent = None
self.parent = parent
self.left = left
self.right = right
self.top = top
self.bottom = bottom
def _update_parent(self, side):
if side not in ["left", "right", "top", "bottom"]:
raise ValueError
if side in ["top", "bottom"] and isinstance(self.parent, SystemLayout):
if eval(f"self.{side}") is not None:
raise ValueError
else:
setattr(self._parent_xml_object, f"xml_{side}_margin", eval(f"self.{side}"))
@property
def bottom(self):
"""
Set and get :obj:`~musicxml.xmlelement.xmlelement.XMLBottomMargin` value of parent.
"""
return self._bottom
@bottom.setter
def bottom(self, val):
self._bottom = val
self._update_parent("bottom")
@property
def left(self) -> Union[int, float]:
"""
Set and get :obj:`~musicxml.xmlelement.xmlelement.XMLLeftMargin` value of parent.
"""
return self._left
@left.setter
def left(self, val):
self._left = val
self._update_parent("left")
@property
def parent(self):
"""
:return: parent layout object. :obj:`PageLayout` and :obj:`SystemLayout` are implemented.
"""
return self._parent
@parent.setter
def parent(self, val):
self._parent = val
if isinstance(val, PageLayout):
self._parent_xml_object = self._parent.xml_object.xml_page_margins
elif isinstance(val, SystemLayout):
self._parent_xml_object = self._parent.xml_object.xml_system_margins
else:
raise NotImplementedError
@property
def right(self) -> Union[int, float]:
"""
Set and get :obj:`~musicxml.xmlelement.xmlelement.XMLRightMargin` value of parent.
"""
return self._right
@right.setter
def right(self, val):
self._right = val
self._update_parent("right")
@property
def top(self) -> Union[int, float]:
"""
Set and get :obj:`~musicxml.xmlelement.xmlelement.XMLTopMargin` value of parent.
"""
return self._top
@top.setter
def top(self, val) -> Union[int, float]:
self._top = val
self._update_parent("top")
[docs]class Scaling(XMLWrapper):
_ATTRIBUTES = {"millimeters", "tenths", "score"}
XMLClass = XMLScaling
def __init__(
self,
millimeters: Union[int, float] = SCALING["millimeters"],
tenths: Union[int, float] = SCALING["tenths"],
):
super().__init__()
self._xml_object = self.XMLClass()
self._millimeters = None
self._tenths = None
self._score = None
self.millimeters = millimeters
self.tenths = tenths
def _update_score(self):
if self.score:
self.score.page_layout._set_page_height_and_width()
@property
def millimeters(self) -> Union[int, float]:
"""
Set and get millimeters value of scaling object. After setting value, parent :obj:`~musicscore.score.Score`'s :obj:`PageLayout` is
updated to reflect the changes.
:return: millimeters
:rtype: Union[int, float]
"""
return self._millimeters
@millimeters.setter
def millimeters(self, val):
if val != self._millimeters:
self._millimeters = val
self.xml_object.xml_millimeters = val
self._update_score()
@property
def score(self):
"""
Set and get parent :obj:`~musicscore.score.Score`. After setting score, its :obj:`~musicxml.xmlelement.xmlelement.XMLScaling`
and :obj:`~musicxml.xmlelement.xmlelement.XMLDefaults` are created if needed.
:return: parent score
:rtype: :obj:`~musicscore.score.Score`
"""
return self._score
@score.setter
def score(self, val):
self._score = val
if not self.score.xml_object.xml_defaults:
self.score.xml_object.xml_defaults = XMLDefaults()
self.score.xml_object.xml_defaults.xml_scaling = self.xml_object
@property
def tenths(self) -> Union[int, float]:
"""
Set and get tenths value of scaling object. After setting value, parent :obj:`~musicscore.score.Score`'s :obj:`PageLayout` is updated to reflect the
changes.
:return: tenths
:rtype: Union[int, float]
"""
return self._tenths
@tenths.setter
def tenths(self, val):
if val != self._tenths:
self._tenths = val
self.xml_object.xml_tenths = val
self._update_score()
[docs] def millimeters_to_tenths(self, x: Union[int, float]) -> Union[int, float]:
"""
Converts millimeter value into tenths
:param x: millimeters
:return: calculated tenths
"""
return round((x * self.tenths) / self.millimeters)
[docs]class PageLayout(XMLWrapper, LayoutMixin):
"""
:param size: :obj:`PAGE_SIZES`
:param orientation: 'portrait', 'landscape'
"""
_ATTRIBUTES = {"scaling", "size", "orientation", "parent"}
XMLClass = XMLPageLayout
def __init__(self, size: str = "A4", orientation: str = "portrait"):
super().__init__()
self._xml_object = self.XMLClass()
self._xml_object.xml_page_margins = XMLPageMargins(type="both")
self._size = None
self._orientation = None
self.size = size
self.orientation = orientation
self._margins = Margins(
parent=self, **PAGE_MARGINS[self.size][self.orientation]
)
def _get_page_height(self):
return (
self.scaling.millimeters_to_tenths(PAGE_SIZES[self.size][1])
if self.orientation == "portrait"
else self.scaling.millimeters_to_tenths(PAGE_SIZES[self.size][0])
)
def _get_page_width(self):
return (
self.scaling.millimeters_to_tenths(PAGE_SIZES[self.size][0])
if self.orientation == "portrait"
else self.scaling.millimeters_to_tenths(PAGE_SIZES[self.size][1])
)
def _set_page_height_and_width(self):
self.xml_object.xml_page_height = self._get_page_height()
self.xml_object.xml_page_width = self._get_page_width()
def _update(self):
self._set_page_height_and_width()
self._margins = Margins(
parent=self, **PAGE_MARGINS[self.size][self.orientation]
)
@property
def margins(self) -> Margins:
"""
Gets margins attribute.
:return: margins object
:rtype: :obj:`Margins`
"""
return self._margins
@property
def orientation(self) -> str:
"""
Set and get orientation. Permitted values are ['portrait', 'landscape']. After setting value, if parent and size already
exist, page's height and width are set.
:return: 'portrait', 'landscape'
:rtype: str
"""
return self._orientation
@orientation.setter
def orientation(self, val):
_permitted = ["portrait", "landscape"]
if val not in _permitted:
raise ValueError(f"{val} can only be: {_permitted}")
if val != self._orientation:
self._orientation = val
if self.size and self.parent:
self._update()
@property
def scaling(self) -> Scaling:
"""
:return: :obj:`~musicscore.score.Score`'s :obj:`Scaling`.
:rtype: :obj:`Scaling`
"""
return self.parent.get_root().scaling
@property
def size(self) -> str:
"""
Set and get orientation. Permitted values are keys of obj:`PAGE_SIZES`. After setting value, if parent and orientation already
exist, page's height and width are set.
:return: sizes in :obj:`PAGE_SIZES`
:rtype: str
"""
return self._size
@size.setter
def size(self, val):
if val not in PAGE_SIZES:
raise NotImplementedError
if val != self._size:
self._size = val
if self.orientation and self.parent:
self._update()
[docs]class SystemLayout(XMLWrapper, LayoutMixin):
_ATTRIBUTES = {"system_distance", "top_system_distance", "parent"}
XMLClass = XMLSystemLayout
def __init__(
self,
system_distance: Union[int, float] = SYSTEM_LAYOUT["system_distance"],
top_system_distance: Union[int, float] = SYSTEM_LAYOUT["top_system_distance"],
):
super().__init__()
self._xml_object = self.XMLClass()
self._xml_object.xml_system_margins = XMLSystemMargins()
self.system_distance = system_distance
self.top_system_distance = top_system_distance
self._margins = Margins(parent=self, **SYSTEM_MARGINS)
@property
def margins(self) -> Margins:
"""
Gets margins attribute.
:return: margins object
:rtype: :obj:`Margins`
"""
return self._margins
@property
def system_distance(self) -> Optional[Union[int, float]]:
"""
Set and get ``value`` of :obj:`~musicxml.xmlelement.xmlelement.XMLSystemDistance`.
:return: ``self.xml_object.xml_system_distance.value_``
:rtype: int, float, None
"""
if self.xml_object.xml_system_distance:
return self.xml_object.xml_system_distance.value_
@system_distance.setter
def system_distance(self, val):
self.xml_object.xml_system_distance = val
@property
def top_system_distance(self) -> Optional[Union[int, float]]:
"""
Set and get ``value`` of :obj:`~musicxml.xmlelement.xmlelement.XMLTopSystemDistance`.
:return: ``self.xml_object.xml_top_system_distance.value_``
:rtype: int, float, None
"""
if self.xml_object.xml_top_system_distance:
return self.xml_object.xml_top_system_distance.value_
@top_system_distance.setter
def top_system_distance(self, val):
self.xml_object.xml_top_system_distance = val
[docs]class StaffLayout(XMLWrapper, LayoutMixin):
_ATTRIBUTES = {"staff_distance", "parent"}
XMLClass = XMLStaffLayout
def __init__(self, staff_distance=STAFF_LAYOUT["staff_distance"]):
super().__init__()
self._xml_object = self.XMLClass()
self.staff_distance = staff_distance
@property
def staff_distance(self) -> Optional[Union[int, float]]:
"""
Set and get ``value_`` of :obj:`~musicxml.xmlelement.xmlelement.XMLStaffDistance`.
:return: ``self.xml_object.xml_staff_distance.value_``
:rtype: int, float, None
"""
if self.xml_object.xml_staff_distance:
return self.xml_object.xml_staff_distance.value_
@staff_distance.setter
def staff_distance(self, val):
self.xml_object.xml_staff_distance = val