You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
11 KiB
292 lines
11 KiB
# -*- coding: utf-8 -*-
|
|
|
|
from xml.etree import ElementTree as ET
|
|
from chordsheet.parsers import parseFingering, parseName
|
|
from reportlab.lib.units import mm
|
|
from reportlab.lib.pagesizes import A4
|
|
|
|
defaultTimeSignature = 4
|
|
|
|
|
|
class Style:
|
|
def __init__(self, **kwargs):
|
|
# set up the style using sane defaults
|
|
self.unit = kwargs.get('unit', mm)
|
|
self.pageSize = kwargs.get('pageSize', A4)
|
|
self.leftMargin = kwargs.get('leftMargin', 10)
|
|
self.topMargin = kwargs.get('topMargin', 10)
|
|
self.rightMargin = kwargs.get('rightMargin', 10)
|
|
self.bottomMargin = kwargs.get('bottomMargin', 10)
|
|
self.font = kwargs.get('font', 'FreeSans')
|
|
self.lineSpacing = kwargs.get('lineSpacing', 1.15)
|
|
self.unitWidth = kwargs.get('unitWidth', 10)
|
|
|
|
self.useIncludedFont = True
|
|
self.numberPages = True
|
|
|
|
self.separatorSize = 5*self.unit
|
|
|
|
self.titleFontSize = 24
|
|
self.subtitleFontSize = 18
|
|
self.creditsFontSize = 12
|
|
self.tempoFontSize = 12
|
|
self.headingFontSize = 18
|
|
self.notesFontSize = 12
|
|
self.chordNameFontSize = 18
|
|
self.beatsFontSize = 12
|
|
|
|
|
|
class Chord:
|
|
def __init__(self, name, **kwargs):
|
|
self.name = name
|
|
self.voicings = {}
|
|
for inst, fing in kwargs.items():
|
|
self.voicings[inst] = fing
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
return self.name == other.name and self.voicings == other.voicings
|
|
return NotImplemented
|
|
|
|
|
|
class Block:
|
|
def __init__(self, length, chord=None, notes=None):
|
|
self.length = length
|
|
self.chord = chord
|
|
self.notes = notes
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
return self.length == other.length and self.chord == other.chord and self.notes == other.notes
|
|
return NotImplemented
|
|
|
|
|
|
class Section:
|
|
def __init__(self, blockList=None, name=None):
|
|
self.blockList = blockList or []
|
|
self.name = name
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
return self.blockList == other.blockList and self.name == other.name
|
|
return NotImplemented
|
|
|
|
|
|
class Document:
|
|
def __init__(self, chordList=None, sectionList=None, title=None, subtitle=None, composer=None, arranger=None, timeSignature=defaultTimeSignature, tempo=None):
|
|
self.chordList = chordList or []
|
|
self.sectionList = sectionList or []
|
|
self.title = title or '' # Do not initialise title empty
|
|
self.subtitle = subtitle
|
|
self.composer = composer
|
|
self.arranger = arranger
|
|
self.timeSignature = timeSignature
|
|
self.tempo = tempo
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
textEqual = self.title == other.title and self.subtitle == other.subtitle and self.composer == other.composer and self.arranger == other.arranger and self.timeSignature == other.timeSignature and self.tempo == other.tempo # check all the text values for equality
|
|
return textEqual and self.chordList == other.chordList and self.sectionList == other.sectionList
|
|
return NotImplemented
|
|
|
|
def loadXML(self, filepath):
|
|
"""
|
|
Read an XML file and import its contents.
|
|
"""
|
|
xmlDoc = ET.parse(filepath)
|
|
root = xmlDoc.getroot()
|
|
|
|
self.chordList = []
|
|
if root.find('chords'):
|
|
for c in root.findall('chords/chord'):
|
|
self.chordList.append(Chord(parseName(c.find('name').text)))
|
|
for v in c.findall('voicing'):
|
|
self.chordList[-1].voicings[v.attrib['instrument']
|
|
] = parseFingering(v.text, v.attrib['instrument'])
|
|
|
|
self.sectionList = []
|
|
if root.find('section'):
|
|
for n, s in enumerate(root.findall('section')):
|
|
blockList = []
|
|
|
|
for b in s.findall('block'):
|
|
blockChordName = parseName(b.find('chord').text) if b.find(
|
|
'chord') is not None else None
|
|
if blockChordName:
|
|
blockChord = None
|
|
for c in self.chordList:
|
|
if c.name == blockChordName:
|
|
blockChord = c
|
|
break
|
|
if blockChord is None:
|
|
raise ValueError("Chord {c} does not match any chord in {l}.".format(
|
|
c=blockChordName, l=self.chordList))
|
|
else:
|
|
blockChord = None
|
|
blockNotes = (b.find('notes').text if b.find(
|
|
'notes') is not None else None)
|
|
blockList.append(
|
|
Block(float(b.find('length').text), chord=blockChord, notes=blockNotes))
|
|
# automatically name the section by its index if a name isn't given. The +1 is because indexing starts from 0.
|
|
self.sectionList.append(Section(blockList=blockList, name=(
|
|
s.attrib['name'] if 'name' in s.attrib else "Section {}".format(n + 1))))
|
|
|
|
self.title = (root.find('title').text if root.find(
|
|
'title') is not None else '') # Do not initialise title empty
|
|
self.subtitle = (root.find('subtitle').text if root.find(
|
|
'subtitle') is not None else None)
|
|
self.composer = (root.find('composer').text if root.find(
|
|
'composer') is not None else None)
|
|
self.arranger = (root.find('arranger').text if root.find(
|
|
'arranger') is not None else None)
|
|
self.timeSignature = (int(root.find('timesignature').text) if root.find(
|
|
'timesignature') is not None else defaultTimeSignature)
|
|
self.tempo = (root.find('tempo').text if root.find(
|
|
'tempo') is not None else None)
|
|
|
|
@classmethod
|
|
def newFromXML(cls, filepath):
|
|
"""
|
|
Create a new Document object directly from an XML file.
|
|
"""
|
|
doc = cls()
|
|
doc.loadXML(filepath)
|
|
return doc
|
|
|
|
def saveXML(self, filepath):
|
|
"""
|
|
Write the contents of the Document object to an XML file.
|
|
"""
|
|
root = ET.Element("chordsheet")
|
|
|
|
ET.SubElement(root, "title").text = self.title
|
|
|
|
if self.subtitle is not None:
|
|
ET.SubElement(root, "subtitle").text = self.subtitle
|
|
|
|
if self.arranger is not None:
|
|
ET.SubElement(root, "arranger").text = self.arranger
|
|
|
|
if self.composer is not None:
|
|
ET.SubElement(root, "composer").text = self.composer
|
|
|
|
ET.SubElement(root, "timesignature").text = str(self.timeSignature)
|
|
|
|
if self.tempo is not None:
|
|
ET.SubElement(root, "tempo").text = self.tempo
|
|
|
|
chordsElement = ET.SubElement(root, "chords")
|
|
|
|
for c in self.chordList:
|
|
chordElement = ET.SubElement(chordsElement, "chord")
|
|
ET.SubElement(chordElement, "name").text = c.name
|
|
for inst in c.voicings.keys():
|
|
if inst == 'guitar':
|
|
ET.SubElement(chordElement, "voicing", attrib={
|
|
'instrument': 'guitar'}).text = ','.join(c.voicings['guitar'])
|
|
if inst == 'piano':
|
|
ET.SubElement(chordElement, "voicing", attrib={
|
|
'instrument': 'piano'}).text = ','.join(c.voicings['piano'])
|
|
|
|
for n, s in enumerate(self.sectionList):
|
|
sectionElement = ET.SubElement(root, "section", attrib={
|
|
'name': s.name if s.name else "Section {}".format(n + 1)})
|
|
for b in s.blockList:
|
|
blockElement = ET.SubElement(sectionElement, "block")
|
|
ET.SubElement(blockElement, "length").text = str(b.length)
|
|
if b.chord is not None:
|
|
ET.SubElement(blockElement, "chord").text = b.chord.name
|
|
if b.notes is not None:
|
|
ET.SubElement(blockElement, "notes").text = b.notes
|
|
|
|
tree = ET.ElementTree(root)
|
|
tree.write(filepath)
|
|
|
|
def loadCSMacro(self, filepath):
|
|
"""
|
|
Read a Chordsheet Macro file and import its contents.
|
|
"""
|
|
self.chordList = []
|
|
self.sectionList = []
|
|
|
|
aliasTable = {}
|
|
|
|
def chord(args):
|
|
argList = args.split(" ")
|
|
chordName = argList.pop(0)
|
|
|
|
self.chordList.append(Chord(parseName(chordName)))
|
|
|
|
argIter = iter(argList)
|
|
subCmdsArgs = list(zip(argIter, argIter))
|
|
|
|
for subCmd, arg in subCmdsArgs:
|
|
if subCmd == "alias":
|
|
aliasTable[arg] = chordName
|
|
else:
|
|
self.chordList[-1].voicings[subCmd] = parseFingering(arg, subCmd)
|
|
|
|
def section(args):
|
|
blockList = []
|
|
|
|
sectionName, blocks = [arg.strip() for arg in args.split("\n", 1)]
|
|
|
|
self.sectionList.append(Section(name=sectionName))
|
|
|
|
for b in blocks.split():
|
|
blockParams = b.split(",")
|
|
blockLength = float(blockParams[1])
|
|
|
|
if blockParams[0] in aliasTable:
|
|
blockChordName = aliasTable[blockParams[0]]
|
|
else:
|
|
blockChordName = blockParams[0]
|
|
|
|
blockChordName = parseName(blockChordName) if blockChordName not in ["NC", "X"] else None
|
|
|
|
blockChord = None
|
|
|
|
if blockChordName:
|
|
for c in self.chordList:
|
|
if c.name == blockChordName:
|
|
blockChord = c
|
|
break
|
|
if blockChord is None:
|
|
raise ValueError("Chord {c} does not match any chord in {l}.".format(
|
|
c=blockChordName, l=self.chordList))
|
|
|
|
blockList.append(Block(blockLength, chord=blockChord))
|
|
|
|
self.sectionList[-1].blockList = blockList
|
|
|
|
with open(filepath, 'r') as f:
|
|
cmatext = f.read()
|
|
|
|
cmaCmdsArgs = [statement.split(" ", 1) for statement in \
|
|
(rawStatement.strip() for rawStatement in cmatext.split("\\")[1:])]
|
|
|
|
for cmd, args in cmaCmdsArgs:
|
|
if cmd == "chordsheet":
|
|
# There's only one version so no need to do anything with this
|
|
pass
|
|
elif cmd == "title":
|
|
self.title = args
|
|
elif cmd == "subtitle":
|
|
self.subtitle = args
|
|
elif cmd == "arranger":
|
|
self.arranger = args
|
|
elif cmd == "composer":
|
|
self.composer = args
|
|
elif cmd == "timesig":
|
|
self.timeSignature = int(args)
|
|
elif cmd == "tempo":
|
|
self.tempo = args
|
|
elif cmd == "chord":
|
|
chord(args)
|
|
elif cmd == "section":
|
|
section(args)
|
|
elif cmd in ["!", "rem"]:
|
|
# Simply ignore comments
|
|
pass
|
|
else:
|
|
raise ValueError(f"Command {cmd} not understood.")
|