Added simple prerender functions to estimate seciton dimensions

Added width-overflow check. Program will downsize the fontSize down untill all lines are readable
Moved config options into the song object
Fixes #3
This commit is contained in:
Marco van Dijk 2021-07-09 00:55:45 +02:00
parent 55daf981cc
commit db9633a855
4 changed files with 106 additions and 61 deletions

View File

@ -36,7 +36,7 @@ def initConfig():
'metaFontWeight': 8, 'metaFontWeight': 8,
'lyricfontfamily': 'fonts/CourierPrime-Regular.ttf', 'lyricfontfamily': 'fonts/CourierPrime-Regular.ttf',
'tablaturefontfamliy': 'fonts/CourierPrime-Bold.ttf', 'tablaturefontfamliy': 'fonts/CourierPrime-Bold.ttf',
'songFontWeight': 14, 'songFontWeight': 18,
'imageWidth': 595, 'imageHeight': 842, # A4 at 72dpi 'imageWidth': 595, 'imageHeight': 842, # A4 at 72dpi
'backgroundColour': '255,255,255', 'backgroundColour': '255,255,255',
'fontColour': '0,0,0', 'fontColour': '0,0,0',

View File

@ -14,6 +14,8 @@
# - Move read functions to separate input functions (also to support more types of inputs) # - Move read functions to separate input functions (also to support more types of inputs)
import re import re
import lib.config
from PIL import ImageFont
"""!@brief Removes empty lines and makes sure every line ends with \r\n """!@brief Removes empty lines and makes sure every line ends with \r\n
@param inputString raw txt input @param inputString raw txt input
@ -82,12 +84,43 @@ class Section:
self.rawData = "" self.rawData = ""
# Flag for succesfully parsed # Flag for succesfully parsed
self.isParsed = False 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 """!@brief Converts raw buffered data into separate Lyric and tablature lines
@return None @return None
""" """
# Parses self.rawData into lyrics and tablature strings # Parses self.rawData into lyrics and tablature strings
def parseMe(self): def initSections(self):
isFirstLine = True isFirstLine = True
# Input sections may have tablature-only or lyric-only sections # 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 # So we have to insert empty lines if we have subsequent tablature or lyric lines
@ -150,11 +183,51 @@ class Song:
self.rawData = "" self.rawData = ""
# Flag for succesfully parsed # Flag for succesfully parsed
self.isParsed = False 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 """!@brief Parses self.rawData into Section objects and metadata
@return None @return None
""" """
def parseMe(self): def initSections(self):
# Get raw data # Get raw data
self.rawData = readSourceFile(self.inputFile) self.rawData = readSourceFile(self.inputFile)
# Clean up input # Clean up input
@ -194,7 +267,7 @@ class Song:
#print("set rawData of '{}' to this section".format(thisSection.rawData)) #print("set rawData of '{}' to this section".format(thisSection.rawData))
parseData = parseData[delimiterIndex:] parseData = parseData[delimiterIndex:]
# Finally parse section data # Finally parse section data
thisSection.parseMe() thisSection.initSections()
if thisSection.isParsed: if thisSection.isParsed:
self.sections.append(thisSection) self.sections.append(thisSection)
else: else:

12
main.py
View File

@ -34,12 +34,20 @@ def main():
songs = lib.initSongs.getSongObjects() songs = lib.initSongs.getSongObjects()
# Convert all songs into sections # Convert all songs into sections
for song in songs: 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 # Parse as PNG a4
if song.isParsed: if song.isParsed:
# Create subdirectory where we will output our images # Create subdirectory where we will output our images
targetDirectory = song.outputLocation + "-a4-png" 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 # Write out metadata and sections, as many as can fit on one page
output2img.outputToImage(targetDirectory, song) output2img.outputToImage(targetDirectory, song)

View File

@ -12,36 +12,10 @@
# - # -
# #
# @section todo TODO # @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 # - Various prints should be printed at specific log levels, to easily switch between debug, info or warnings only
import lib.config
import os import os
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw
"""!@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
"""!@brief Exports the song object to images """!@brief Exports the song object to images
This function renders the metadata and sections 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 It will overwrite existing images, but will not clear old images
@param folderLocation path to where we want the images @param folderLocation path to where we want the images
@param songObj lib.dataStructures.Song object @param songObj lib.dataStructures.Song object
@param configObj configparser object
@return None @return None
""" """
def outputToImage(folderLocation, songObj): def outputToImage(folderLocation, songObj):
@ -61,24 +34,12 @@ def outputToImage(folderLocation, songObj):
#else: #else:
#print("Directory " , folderLocation , " already exists") #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 # Init image info
imageNumber = 1 imageNumber = 1
currentHeight = topMargin currentHeight = songObj.topMargin
# New Image # New Image
a4image = Image.new('RGB',(imageWidth, imageHeight),(backgroundColour)) a4image = Image.new('RGB',(songObj.imageWidth, songObj.imageHeight),(songObj.backgroundColour))
draw = ImageDraw.Draw(a4image) draw = ImageDraw.Draw(a4image)
# Write metadata # Write metadata
@ -88,11 +49,11 @@ def outputToImage(folderLocation, songObj):
if not line: if not line:
continue continue
#print("meta line '{}'".format(line)) #print("meta line '{}'".format(line))
metadataTextWidth, metadataTextHeight = fontMetadata.getsize(line) metadataTextWidth, metadataTextHeight = songObj.fontMetadata.getsize(line)
draw.text((leftMargin,currentHeight), line, fill=metadataColour, font=fontMetadata) draw.text((songObj.leftMargin,currentHeight), line, fill=songObj.metadataColour, font=songObj.fontMetadata)
currentHeight += metadataTextHeight currentHeight += metadataTextHeight
# Margin between metadata and the first section # Margin between metadata and the first section
currentHeight += topMargin currentHeight += songObj.topMargin
# Iterate over each section # Iterate over each section
# NOTE: sections might be split into lists of pages containing a list of sections # 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 # 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)): 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)) 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 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 # 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") #print("overflow! starting with a new image")
outputLocation = folderLocation + "/" + str(imageNumber) + ".png" outputLocation = folderLocation + "/" + str(imageNumber) + ".png"
imageNumber += 1 imageNumber += 1
a4image.save(outputLocation) a4image.save(outputLocation)
currentHeight = topMargin currentHeight = songObj.topMargin
a4image = Image.new('RGB',(imageWidth, imageHeight),(backgroundColour)) a4image = Image.new('RGB',(songObj.imageWidth, songObj.imageHeight),(songObj.backgroundColour))
draw = ImageDraw.Draw(a4image) draw = ImageDraw.Draw(a4image)
# write section title # write section title
headerWidth, headerHeight = fontTablature.getsize(section.header) headerWidth, headerHeight = songObj.fontTablature.getsize(section.header)
draw.text((leftMargin,currentHeight), section.header, fill=fontColour, font=fontTablature) draw.text((songObj.leftMargin,currentHeight), section.header, fill=songObj.fontColour, font=songObj.fontTablature)
currentHeight += headerHeight currentHeight += headerHeight
# Write each line tablature&lyric data # Write each line tablature&lyric data
while lineIterator < amountOfLines: while lineIterator < amountOfLines:
#print("Printing tablatures line {} and lyrics line {}".format(section.tablatures[lineIterator], section.lyrics[lineIterator])) #print("Printing tablatures line {} and lyrics line {}".format(section.tablatures[lineIterator], section.lyrics[lineIterator]))
# Get tablatures&lyric line # Get tablatures&lyric line
lyricTextWidth, lyricTextHeight = fontLyrics.getsize(section.lyrics[lineIterator]) lyricTextWidth, lyricTextHeight = songObj.fontLyrics.getsize(section.lyrics[lineIterator])
tablatureTextWidth, tablatureTextHeight = fontTablature.getsize(section.tablatures[lineIterator]) tablatureTextWidth, tablatureTextHeight = songObj.fontTablature.getsize(section.tablatures[lineIterator])
# add to image file # 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 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 currentHeight += lyricTextHeight
lineIterator += 1 lineIterator += 1
#print("currentheight={}".format(currentHeight)) #print("currentheight={}".format(currentHeight))
# Margin between each section # 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 # No more sections left, so the current buffered image is ready to be written to file
outputLocation = folderLocation + "/" + str(imageNumber) + ".png" outputLocation = folderLocation + "/" + str(imageNumber) + ".png"
a4image.save(outputLocation) a4image.save(outputLocation)