Skip to content

Commit

Permalink
fix for #289 and other fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jmmelko committed Apr 16, 2024
1 parent 4ff377a commit f18c8e8
Show file tree
Hide file tree
Showing 20 changed files with 289 additions and 56 deletions.
36 changes: 20 additions & 16 deletions src/skymusic/FileUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,24 @@ def shortest_path(path, start_path):


def file_status(file_path):
"""Returns an integer depending on file statis"""
"""Returns an integer depending on file status"""
isfile = os.path.isfile(file_path)

if isfile:
isfile=1 #Definitely a file in the filesystem
isfile = 1 # Rigorously a file in the filesystem
else:
isfile = -1
bare_ext = os.path.splitext(file_path)[1].strip('.')
if not bare_ext.startswith(' ') and (2 <= len(bare_ext) <= 4):
isfile = -1 # maybe a file, but path is invalid (typo)
closest = closest_file(file_path)
if closest:
(file_path, isfile) = (closest, 1)

closest, score = closest_file(file_path)

file_suspect = (not bare_ext.startswith(' ') and (2 <= len(bare_ext) <= 4)) # has a file extension, not a pause . followed by a musical note

if closest and (score > 0.8 or (score > 0.6 and file_suspect)):

(file_path, isfile) = (closest, 1) # is a file, but path was invalid (typo)
else:
isfile = 0 #Not a file attempt
isfile = 0 # Not a file

return (file_path, isfile)

Expand Down Expand Up @@ -68,19 +72,19 @@ def closest_file(filepath):
filedir, filename = os.path.split(filepath)

best_file = None
best_score = 99
best_score = 0
score_threshold = 0.5
for (root, dirs, files) in os.walk(filedir):
for file in files:
bare_file = __strip_accents__(os.path.splitext(os.path.splitext(file)[0])[0]).lower()
bare_filename = __strip_accents__(os.path.splitext(os.path.splitext(filename)[0])[0]).lower()
d = edit_distance(bare_file,bare_filename)
d = __edit_distance__(bare_file,bare_filename)
score = 1 - (d/max(len(bare_file),len(bare_filename)))
#print(f"d({file,filename})={d}")
if d < 3 or (d<4 and len(filename)>10):
if d < best_score:
best_file = os.path.join(root, file)
best_score = d
if score > score_threshold and score > best_score:
best_file, best_score = os.path.join(root, file), score

return best_file
return (best_file, best_score)

#% Levenshtein

Expand Down Expand Up @@ -130,6 +134,6 @@ def __levenshtein__(s, t):

return res

def edit_distance(s, t):
def __edit_distance__(s, t):

return __levenshtein__(s,t)
38 changes: 35 additions & 3 deletions src/skymusic/Lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,44 @@
locales = ['en_US', 'fr_FR', 'vi_VN', 'zh_HANS']
substitutes = {'fr': 'fr_FR', 'en': 'en_US', 'vn': 'vi_VN', 'zh': 'zh_HANS', 'zh_CN': 'zh_HANS'}

'''Unicode ranges for non Asian characters'''
CHAR_RANGES = {'el': [0x370, 0x3ff], 'pl': [0x0, 0x24b], 'ar': [0x600, 0x7ff], 'he': [0x591, 0x5f4], 'th': [0xe01, 0xe5b],'ru': [0x400, 0x527], 'uk': [0x400, 0x527], 'hy': [0x531, 0x58a], 'ka': [0x10a0, 0x10fc]}

LANG = dict()
loaded = dict((locale, False) for locale in locales)
warn_count = 0

def family_from_text(text):
'''Determines locale prefix from some text, analysing unicode characters positions'''
m = 0
text = re.sub('[^\w]+','',text) #Remove non-word characters
text = re.sub('[A-Za-z]+','',text) #Removes letters, including accented ones, keeping accents isolated, to move average away from zero
if not text:
m = 0
return ''
else:
m = sum([int(ord(c)) for c in text])/len(text)
for loc, range in CHAR_RANGES.items():
if m>range[0] and m<range[1]:
return loc.lower()
return ''


def sanitize_locale(locale):
'''Sanitization: correct case and replacement of - in case the locale is a IETF language tag'''
matchobj = re.match(r'([^_-]*)[_|-]*([^_-]*)', locale.strip())
locale = '_'.join(filter(None,(matchobj.group(1).lower(), matchobj.group(2).upper())))
return locale

def get_locale_family(locale):

matchobj = re.match(r'([^_-]*)[_|-]*([^_-]*)', locale.strip())
if matchobj:
family = matchobj.group(1).lower()
else:
family = ''
return family

def check_locale(locale):
try:
locale = locale.split('.')[0]
Expand All @@ -18,9 +52,7 @@ def check_locale(locale):
print(f"\n***WARNING: locale code '{locale}' too short")
return None

# Sanitization: correct case and replacement of - in case the locale is a IETF language tag
matchobj = re.match(r'([^_-]*)[_|-]*([^_-]*)', locale.strip())
locale = '_'.join(filter(None,(matchobj.group(1).lower(), matchobj.group(2).upper())))
locale = sanitize_locale(locale)

if locale not in locales:
substitute = find_substitute(locale)
Expand Down
10 changes: 10 additions & 0 deletions src/skymusic/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ def __init__(self):
self.is_silent = True
self.is_broken = False

@property
def is_tonal(self): return self.get_is_tonal()

@property
def is_textual(self): return self.get_is_textual()

def get_type(self):
return self.type

Expand Down Expand Up @@ -222,6 +228,10 @@ def set_lyric(self, lyric):
elif (len(star_match.group('start')) == 1 and len(star_match.group('end')) == 1):
self.emphasis = 'i'

def get_text(self, *args, **kwargs): return self.get_lyric(*args, **kwargs)

def set_text(self, *args, **kwargs): return self.set_lyric(*args, **kwargs)

def __len__(self):
return len(self.lyric)

Expand Down
2 changes: 1 addition & 1 deletion src/skymusic/parsers/song_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def set_note_parser(self, input_mode=None):
if input_mode is None: input_mode = self.input_mode

if input_mode is None:
raise SongParserError("cannot set NoteParser: Invalid input_mode {input_mode}")
raise SongParserError(f"cannot set NoteParser: Invalid input_mode '{input_mode}'")
else:
self.note_parser = self.get_note_parser(input_mode)

Expand Down
32 changes: 21 additions & 11 deletions src/skymusic/renderers/instrument_renderers/png_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from skymusic.resources import Resources
from skymusic.renderers.note_renderers import png_nr
from skymusic.modes import GamePlatform
from skymusic import Lang

try:
from PIL import Image, ImageDraw, ImageFont
Expand All @@ -28,16 +29,30 @@ def __init__(self, locale=None, harp_type='harp', platform_name=GamePlatform.get

self.text_bkg = Resources.PNG_SETTINGS['text_bkg'] # Transparent white
self.song_bkg = Resources.PNG_SETTINGS['song_bkg'] # White paper sheet
self.font_color = Resources.PNG_SETTINGS['font_color']
self.font_path = Resources.PNG_SETTINGS['font_path']
self.font_color = Resources.PNG_SETTINGS['font_color']
self.harp_font_size = Resources.PNG_SETTINGS['harp_font_size']
self.font_size = Resources.PNG_SETTINGS['font_size']
self.h1_font_size = Resources.PNG_SETTINGS['h1_font_size']
self.h2_font_size = Resources.PNG_SETTINGS['h2_font_size']
self.font_path = Resources.PNG_SETTINGS['font_path']
self.repeat_height = None
self.voice_font_size = Resources.PNG_SETTINGS['voice_font_size']
self.hr_color = Resources.PNG_SETTINGS['hr_color'] # Grey or White

# Load default font from locale
self.font_path = Resources.load_font(locale)
self.set_fonts()

self.harp_size = None
self.gp_note_size = None
self.harp_type = harp_type
self.empty_harp_png = Resources.PNGS[platform_name][f'empty-{harp_type}']
self.unhighlighted_harp_png = Resources.PNGS[platform_name][f'unhighlighted-{harp_type}']


def set_fonts(self, font_path=None):

if not font_path: font_path = self.font_path

if not no_PIL_module:
try:
self.voice_font = ImageFont.truetype(self.font_path, self.voice_font_size)
Expand All @@ -52,11 +67,6 @@ def __init__(self, locale=None, harp_type='harp', platform_name=GamePlatform.get
self.h2_font = ImageFont.load_default()
self.text_font = ImageFont.load_default()

self.harp_size = None
self.gp_note_size = None
self.harp_type = harp_type
self.empty_harp_png = Resources.PNGS[platform_name][f'empty-{harp_type}']
self.unhighlighted_harp_png = Resources.PNGS[platform_name][f'unhighlighted-{harp_type}']

def trans_paste(self, bg, fg, box=(0, 0)):
if fg.mode == 'RGBA':
Expand Down Expand Up @@ -141,8 +151,8 @@ def get_gamepad_gaps(self):

return {k:round(v*(self.gp_note_size[0] if k.endswith('H') else self.gp_note_size[1])) for k,v in self.gamepad_gaps.items()}


def get_text_size(self, fnt=None, text='HQfgjyp', rescale=1):
def get_text_size(self, fnt=None, text=u'HQfgjypŹỵ', rescale=1):
"""Calculates the height of the voices based on a standard text and the font size"""
#fnt = ImageFont.truetype(self.font_path, self.voice_font_size)
fnt = fnt if fnt else self.voice_font
Expand Down Expand Up @@ -206,7 +216,7 @@ def render_voice(self, instrument, rescale=1.0, max_size=None):
fnt = self.__scaled_font__(self.voice_font_size, rescale)
else:
fnt = self.voice_font

lyric_width, lyric_height = self.get_text_size(fnt, lyric)

voice_render = Image.new('RGBA', (lyric_width, lyric_height), color=self.text_bkg)
Expand Down
2 changes: 1 addition & 1 deletion src/skymusic/renderers/song_renderers/midi_sr.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def _copyright_track_(self, mid, song):
if artist: artist = artist[1]
transcript = song_meta['transcript']
if transcript: transcript = transcript[1]
copytrack.append(mido.MetaMessage('copyright', text=artist+'/'+transcript))
copytrack.append(mido.MetaMessage('copyright', text=ascii(artist)+'/'+ascii(transcript)))

return mid

Expand Down
61 changes: 43 additions & 18 deletions src/skymusic/renderers/song_renderers/png_sr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from skymusic.resources import Resources
from skymusic.modes import GamePlatform
from skymusic.sheetlayout import Ruler
from skymusic import Lang

try:
from PIL import Image, ImageDraw, ImageFont
Expand Down Expand Up @@ -43,18 +44,10 @@ def __init__(self, locale=None, aspect_ratio=16/9.0, gamepad=None, theme=Resourc
self.font_path = Resources.PNG_SETTINGS['font_path']
self.text_bkg = Resources.PNG_SETTINGS['text_bkg'] # Transparent white

if not no_PIL_module:
try:
self.h1_font = ImageFont.truetype(self.font_path, self.h1_font_size)
self.h2_font = ImageFont.truetype(self.font_path, self.h2_font_size)
self.text_font = ImageFont.truetype(self.font_path, self.font_size)
self.dimmed_text_font = ImageFont.truetype(self.font_path, self.font_size)
except OSError:
self.h1_font = ImageFont.load_default()
self.h2_font = ImageFont.load_default()
self.text_font = ImageFont.load_default()
self.dimmed_text_font = ImageFont.load_default()

# Load default font from locale
Resources.load_font(locale)
self.font_path = Resources.PNG_SETTINGS['font_path']
self.set_fonts()

# INSTRUMENT RESIZING
self._max_harps_line_ = round(Resources.PNG_SETTINGS['max_harps_line']*aspect_ratio/(16/9.0))
Expand All @@ -71,8 +64,7 @@ def __init__(self, locale=None, aspect_ratio=16/9.0, gamepad=None, theme=Resourc
self._gp_max_upscale_ = 1

if not no_PIL_module:
self.switch_harp('harp') #sets _harp_size0_, _harp_spacings0_, _voice_size0_

self.switch_harp('harp') #sets _harp_size0_, _harp_spacings0_, _voice_size0_

self._gp_note_size_ = None
self._gamepad_spacings_ = tuple()
Expand All @@ -86,6 +78,37 @@ def __init__(self, locale=None, aspect_ratio=16/9.0, gamepad=None, theme=Resourc
self.set_gamepad_spacings()
self.set_gamepad_rescale(self._max_gp_notes_line_) #Called once and for all

def set_fonts(self, font_path=None):

if not font_path: font_path = self.font_path

if not no_PIL_module:
try:
self.h1_font = ImageFont.truetype(font_path, self.h1_font_size)
self.h2_font = ImageFont.truetype(font_path, self.h2_font_size)
self.text_font = ImageFont.truetype(font_path, self.font_size)
self.dimmed_text_font = ImageFont.truetype(font_path, self.font_size)
except OSError:
self.h1_font = ImageFont.load_default()
self.h2_font = ImageFont.load_default()
self.text_font = ImageFont.load_default()
self.dimmed_text_font = ImageFont.load_default()
print('***ERROR: could not load font '+font_path)


def check_set_fonts(self, song):
'''Tries to identify alphabet from text'''
text = ""
lines = song.get_textual_lines()
for line in lines:
for voice in line:
text += voice.get_lyric()
family = Lang.family_from_text(text)
self.font_path = Resources.load_font(family)
self.set_fonts()
if family not in self.locale:
self.locale = family


def switch_harp(self, harp_type):

Expand Down Expand Up @@ -180,8 +203,7 @@ def get_nontonal_spacings(self, rescale=1):

def get_text_height(self, fnt, rescale=1):
"""Calculates the text height in PNG for a standard text depending on the input font size"""
return round(fnt.getsize('HQfgjyp')[1]*rescale)

return round(fnt.getsize(u'HQfgjypŹỵ')[1]*rescale)


def trans_paste(self, bg, fg, box=(0, 0)):
Expand Down Expand Up @@ -221,9 +243,9 @@ def get_num_png(self, num, fit_size=None, rescale=1):
num_im = num_im.resize((round(num_im.size[0] * rescale), round(num_im.size[1] * rescale)), resample=Image.LANCZOS)
return num_im


def write_header(self, song_render, filenum, song, x_in_png, y_in_png):

def write_header(self, song_render, filenum, song, x_in_png, y_in_png):

#harp_type = song.get_harp_type()
#self.switch_harp(harp_type)
text_rescale = 1 #self.get_text_rescale() #Hard-coded value
Expand Down Expand Up @@ -284,6 +306,9 @@ def write_header(self, song_render, filenum, song, x_in_png, y_in_png):

def write_buffers(self, song, start_row=0, start_col=0, tonal_row=0, buffer_list=None):

# Select right font according to unicode characters
self.check_set_fonts(song)

if buffer_list is None:
buffer_list = []
global no_PIL_module
Expand Down
40 changes: 34 additions & 6 deletions src/skymusic/resources/Resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,40 @@ def load_theme(theme, platform='mobile'):
'max_gp_notes_line':20,
}

try:
with importlib_resources.path(fonts, 'NotoSansCJKjp-Bold.otf') as fp:
PNG_SETTINGS['font_path'] = str(fp)
except FileNotFoundError:
PNG_SETTINGS['font_path'] = os.path.join(os.path.dirname(fonts.__file__), 'NotoSansCJKjp-Bold.otf')
print(f"***ERROR: Could not find: 'fonts/{os.path.relpath(PNG_SETTINGS['font_path'], start=os.path.dirname(fonts.__file__))}'")
FONTS = {
'asian': 'NotoSansCJKjp-Bold.otf',
'ja': 'NotoSansCJKjp-Bold.otf',
'zh': 'NotoSansCJKjp-Bold.otf',
'ko': 'NotoSansCJKjp-Bold.otf',
'latin': 'NotoSans-Bold.ttf',
'pl': 'NotoSans-Bold.ttf',
'el': 'NotoSans-Bold.ttf',
'ar': 'NotoSansArabic-Bold.ttf',
'he': 'NotoSansHebrew-Bold.ttf',
'th': 'NotoSansThai-Bold.ttf',
'cyrillic': 'NotoSans-Bold.ttf',
'ru': 'NotoSans-Bold.ttf',
'uk': 'NotoSans-Bold.ttf',
'vi': 'NotoSans-Bold.ttf',
'hy': 'NotoSansArmenian-Bold.ttf',
'ka': 'NotoSansGeorgian-Bold.ttf',
}

def load_font(locale='ja'):

# Keep only locale prefix
matchobj = re.match(r'([^_-]*)[_|-]*([^_-]*)', locale.strip())
if matchobj: locale = matchobj.group(1).lower()
if locale not in FONTS: locale = list(FONTS)[0]

try:
with importlib_resources.path(fonts, FONTS[locale]) as fp:
PNG_SETTINGS['font_path'] = str(fp)
except FileNotFoundError:
PNG_SETTINGS['font_path'] = os.path.join(os.path.dirname(fonts.__file__), FONTS[locale])
print(f"***ERROR: Could not find: 'fonts/{os.path.relpath(PNG_SETTINGS['font_path'], start=os.path.dirname(fonts.__file__))}'")

return PNG_SETTINGS['font_path']

MAX_FILENAME_LENGTH = 127
MAX_NUM_FILES = 15
Expand Down
Binary file added src/skymusic/resources/fonts/NotoSans-Bold.ttf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit f18c8e8

Please sign in to comment.