diff --git a/lib/config.py b/lib/config.py index e5e079c..8376881 100644 --- a/lib/config.py +++ b/lib/config.py @@ -36,7 +36,7 @@ def initConfig(): 'metaFontWeight': 8, 'lyricfontfamily': 'fonts/CourierPrime-Regular.ttf', 'tablaturefontfamliy': 'fonts/CourierPrime-Bold.ttf', - 'songFontWeight': 14, + 'songFontWeight': 18, 'imageWidth': 595, 'imageHeight': 842, # A4 at 72dpi 'backgroundColour': '255,255,255', 'fontColour': '0,0,0', diff --git a/lib/dataStructures.py b/lib/dataStructures.py index 3708fe6..8438f6a 100644 --- a/lib/dataStructures.py +++ b/lib/dataStructures.py @@ -14,6 +14,8 @@ # - Move read functions to separate input functions (also to support more types of inputs) import re +import lib.config +from PIL import ImageFont """!@brief Removes empty lines and makes sure every line ends with \r\n @param inputString raw txt input @@ -82,12 +84,43 @@ class Section: self.rawData = "" # Flag for succesfully parsed self.isParsed = False + # Expected dimensions of this section + self.expectedWidth = -1 + self.expectedHeight = -1 + + """!@brief Calculates dimensions of rendered text + This function calculates the dimensions of each line of text + the section contains and sets the internal variables + @param section lib.dataStructures.Section object + @return None + """ + def calculateSectionDimensions(self, fontTablature, fontLyrics): + lineIterator = 0 + amountOfLines = len(self.lyrics) + heightSum = 0 + maxWidth = 0 + # consider section title + headerWidth, headerHeight = fontTablature.getsize(self.header) + heightSum += headerHeight + maxWidth = headerWidth + while lineIterator < amountOfLines: + # Get chord&lyric line dimensions + lyricTextWidth, lyricTextHeight = fontLyrics.getsize(self.lyrics[lineIterator]) + tablatureTextWidth, chordTextHeight = fontTablature.getsize(self.tablatures[lineIterator]) + heightSum += lyricTextHeight + chordTextHeight + if lyricTextWidth > maxWidth: + maxWidth = lyricTextWidth + if tablatureTextWidth > maxWidth: + maxWidth = tablatureTextWidth + lineIterator += 1 + self.expectedWidth = maxWidth + self.expectedHeight = heightSum """!@brief Converts raw buffered data into separate Lyric and tablature lines @return None """ # Parses self.rawData into lyrics and tablature strings - def parseMe(self): + def initSections(self): isFirstLine = True # Input sections may have tablature-only or lyric-only sections # So we have to insert empty lines if we have subsequent tablature or lyric lines @@ -150,11 +183,51 @@ class Song: self.rawData = "" # Flag for succesfully parsed self.isParsed = False + configObj = lib.config.config['output'] + self.topMargin = int(configObj['topMargin']) + self.fontColour = tuple(int(var) for var in configObj['fontColour'].split(',')) + self.backgroundColour = tuple(int(var) for var in configObj['backgroundColour'].split(',')) + self.metadataColour = tuple(int(var) for var in configObj['metadataColour'].split(',')) + self.imageWidth = int(configObj['imageWidth']) + self.imageHeight = int(configObj['imageHeight']) + self.leftMargin = int(configObj['leftMargin']) + self.fontMetadata = ImageFont.truetype(configObj['metafontfamily'], int(configObj['metaFontWeight'])) + self.fontSize = int(configObj['songFontWeight']) + self.fontLyrics = ImageFont.truetype(configObj['lyricfontfamily'], self.fontSize) + self.fontTablature = ImageFont.truetype(configObj['tablaturefontfamliy'], self.fontSize) + self.configObj = configObj + + """!@brief Calculates the expected dimensions of all sections + @return None + """ + def prerenderSections(self): + for section in self.sections: + section.calculateSectionDimensions(self.fontTablature, self.fontLyrics) + + """!@brief Checks whether we are overflowing on the width of the page + @return True if everything OK, False if overflowing + """ + def checkOverflowX(self): + for section in self.sections: + if section.expectedWidth > self.imageWidth: + return False + return True + + """!@brief Resizes all sections by a specified amount + Also recalculates all section sizes afterwards + @param mutator amount of fontSize to add/dec from current font size + @return None + """ + def resizeAllSections(self, mutator): + self.fontSize += mutator + self.fontLyrics = ImageFont.truetype(self.configObj['lyricfontfamily'], self.fontSize) + self.fontTablature = ImageFont.truetype(self.configObj['tablaturefontfamliy'], self.fontSize) + self.prerenderSections() """!@brief Parses self.rawData into Section objects and metadata @return None """ - def parseMe(self): + def initSections(self): # Get raw data self.rawData = readSourceFile(self.inputFile) # Clean up input @@ -194,7 +267,7 @@ class Song: #print("set rawData of '{}' to this section".format(thisSection.rawData)) parseData = parseData[delimiterIndex:] # Finally parse section data - thisSection.parseMe() + thisSection.initSections() if thisSection.isParsed: self.sections.append(thisSection) else: diff --git a/main.py b/main.py index 75a071c..55e90e5 100644 --- a/main.py +++ b/main.py @@ -34,12 +34,20 @@ def main(): songs = lib.initSongs.getSongObjects() # Convert all songs into sections for song in songs: - song.parseMe() + print("Start parsing of file '{}'...".format(song.inputFile)) + # Initialise internal data structures + song.initSections() + # Prerender: calculate the expected dimensions for each section + song.prerenderSections() + # While we overflow on X: resize all sections down and recalculate + while not song.checkOverflowX(): + #print("Overflowing on width of the page. Decreasing font size by 2...") + song.resizeAllSections(-2) # Parse as PNG a4 if song.isParsed: # Create subdirectory where we will output our images targetDirectory = song.outputLocation + "-a4-png" - print("Successfully parsed '{}' file. Writing output to '{}'".format(song.inputFile, targetDirectory)) + print("Successfully parsed file. Writing output to '{}'".format(song.inputFile, targetDirectory)) # Write out metadata and sections, as many as can fit on one page output2img.outputToImage(targetDirectory, song) diff --git a/output2img.py b/output2img.py index 0090eac..44aa183 100644 --- a/output2img.py +++ b/output2img.py @@ -12,36 +12,10 @@ # - # # @section todo TODO -# - A lot of this stuff is hardcoded. We want to write default fonts, sizes, colours, margins, dimensions, wanted amount of pages -# to a config file on first boot. Overwrite these if they get passed via CMD arguments (or manually by user) # - Various prints should be printed at specific log levels, to easily switch between debug, info or warnings only -import lib.config import os -from PIL import Image, ImageDraw, ImageFont - -"""!@brief Calculates the height of rendered text - This function calculates the dimensions of each line of text - the section contains and returns the sum - @param section lib.dataStructures.Section object - @param fontTablature ImageFont.truetype object of font we are using for tablature lines - @param fontLyrics ImageFont.truetype object of font we are using for lyric lines - @return the total height of the section -""" -def calcSectionHeight(section, fontTablature, fontLyrics): - lineIterator = 0 - amountOfLines = len(section.lyrics) - heightSum = 0 - # consider section title - headerWidth, headerHeight = fontTablature.getsize(section.header) - heightSum += headerHeight - while lineIterator < amountOfLines: - # Get chord&lyric line dimensions - lyricTextWidth, lyricTextHeight = fontLyrics.getsize(section.lyrics[lineIterator]) - tablatureTextWidth, chordTextHeight = fontTablature.getsize(section.tablatures[lineIterator]) - heightSum += lyricTextHeight + chordTextHeight - lineIterator += 1 - return heightSum +from PIL import Image, ImageDraw """!@brief Exports the song object to images This function renders the metadata and sections @@ -50,7 +24,6 @@ def calcSectionHeight(section, fontTablature, fontLyrics): It will overwrite existing images, but will not clear old images @param folderLocation path to where we want the images @param songObj lib.dataStructures.Song object - @param configObj configparser object @return None """ def outputToImage(folderLocation, songObj): @@ -60,25 +33,13 @@ def outputToImage(folderLocation, songObj): print("Directory " , folderLocation , " Created ") #else: #print("Directory " , folderLocation , " already exists") - - configObj = lib.config.config['output'] - topMargin = int(configObj['topMargin']) - fontColour = tuple(int(var) for var in configObj['fontColour'].split(',')) - backgroundColour = tuple(int(var) for var in configObj['backgroundColour'].split(',')) - metadataColour = tuple(int(var) for var in configObj['metadataColour'].split(',')) - imageWidth = int(configObj['imageWidth']) - imageHeight = int(configObj['imageHeight']) - leftMargin = int(configObj['leftMargin']) - fontMetadata = ImageFont.truetype(configObj['metafontfamily'], int(configObj['metaFontWeight'])) - fontLyrics = ImageFont.truetype(configObj['lyricfontfamily'], int(configObj['songFontWeight'])) - fontTablature = ImageFont.truetype(configObj['tablaturefontfamliy'], int(configObj['songFontWeight'])) # Init image info imageNumber = 1 - currentHeight = topMargin + currentHeight = songObj.topMargin # New Image - a4image = Image.new('RGB',(imageWidth, imageHeight),(backgroundColour)) + a4image = Image.new('RGB',(songObj.imageWidth, songObj.imageHeight),(songObj.backgroundColour)) draw = ImageDraw.Draw(a4image) # Write metadata @@ -88,11 +49,11 @@ def outputToImage(folderLocation, songObj): if not line: continue #print("meta line '{}'".format(line)) - metadataTextWidth, metadataTextHeight = fontMetadata.getsize(line) - draw.text((leftMargin,currentHeight), line, fill=metadataColour, font=fontMetadata) + metadataTextWidth, metadataTextHeight = songObj.fontMetadata.getsize(line) + draw.text((songObj.leftMargin,currentHeight), line, fill=songObj.metadataColour, font=songObj.fontMetadata) currentHeight += metadataTextHeight # Margin between metadata and the first section - currentHeight += topMargin + currentHeight += songObj.topMargin # Iterate over each section # NOTE: sections might be split into lists of pages containing a list of sections # This change will occur when we add an arranger which resizes sections to fit pages better @@ -103,34 +64,37 @@ def outputToImage(folderLocation, songObj): if (amountOfLines != len(section.tablatures)): print("Cannot write this section to file, since it was not processed correctly. There are {} tablature lines and {} lyric lines. Aborting...".format(len(section.chords), amountOfLines)) return + if (section.expectedHeight == -1 or section.expectedWidth == -1): + print("Cannot write this section to file, since it was not processed correctly. The expected dimensions are not set. Aborting...") + return # See if the section would fit on the current page - if it does not, write current buffered image & make the next image ready - if currentHeight + calcSectionHeight(section, fontTablature, fontLyrics) > imageHeight: + if currentHeight + section.expectedHeight > songObj.imageHeight: #print("overflow! starting with a new image") outputLocation = folderLocation + "/" + str(imageNumber) + ".png" imageNumber += 1 a4image.save(outputLocation) - currentHeight = topMargin - a4image = Image.new('RGB',(imageWidth, imageHeight),(backgroundColour)) + currentHeight = songObj.topMargin + a4image = Image.new('RGB',(songObj.imageWidth, songObj.imageHeight),(songObj.backgroundColour)) draw = ImageDraw.Draw(a4image) # write section title - headerWidth, headerHeight = fontTablature.getsize(section.header) - draw.text((leftMargin,currentHeight), section.header, fill=fontColour, font=fontTablature) + headerWidth, headerHeight = songObj.fontTablature.getsize(section.header) + draw.text((songObj.leftMargin,currentHeight), section.header, fill=songObj.fontColour, font=songObj.fontTablature) currentHeight += headerHeight # Write each line tablature&lyric data while lineIterator < amountOfLines: #print("Printing tablatures line {} and lyrics line {}".format(section.tablatures[lineIterator], section.lyrics[lineIterator])) # Get tablatures&lyric line - lyricTextWidth, lyricTextHeight = fontLyrics.getsize(section.lyrics[lineIterator]) - tablatureTextWidth, tablatureTextHeight = fontTablature.getsize(section.tablatures[lineIterator]) + lyricTextWidth, lyricTextHeight = songObj.fontLyrics.getsize(section.lyrics[lineIterator]) + tablatureTextWidth, tablatureTextHeight = songObj.fontTablature.getsize(section.tablatures[lineIterator]) # add to image file - draw.text((leftMargin,currentHeight), section.tablatures[lineIterator], fill=fontColour, font=fontTablature) + draw.text((songObj.leftMargin,currentHeight), section.tablatures[lineIterator], fill=songObj.fontColour, font=songObj.fontTablature) currentHeight += tablatureTextHeight - draw.text((leftMargin,currentHeight), section.lyrics[lineIterator], fill=fontColour, font=fontLyrics) + draw.text((songObj.leftMargin,currentHeight), section.lyrics[lineIterator], fill=songObj.fontColour, font=songObj.fontLyrics) currentHeight += lyricTextHeight lineIterator += 1 #print("currentheight={}".format(currentHeight)) # Margin between each section - currentHeight += topMargin + currentHeight += songObj.topMargin # No more sections left, so the current buffered image is ready to be written to file outputLocation = folderLocation + "/" + str(imageNumber) + ".png" a4image.save(outputLocation)