vxSDK/vxsdk/core/conv/type/font.py

467 lines
14 KiB
Python

"""
Vhex font converter
"""
from PIL import Image
from core.logger import log
__all__ = [
'conv_font_generate'
]
#---
# Internal vxconv.tmol handling routines
#---
def __font_fetch_info(asset):
""" Check and fetch font information
@arg
> asset (VxAsset) - asset information
@return
> dictionary with font information
"""
# generate font default information
font_info = {
# user can customise
'charset' : 'normal',
'grid_size_x' : 0,
'grid_size_y' : 0,
'grid_padding' : 1,
'grid_border' : 1,
'is_proportional' : False,
'line_height' : 0,
'char_spacing' : 1,
# generated "on-the-fly" by the conversion step
# @notes
# This is mainly to provide cache for the Vhex operating system to
# speed-up render calculation by avoiding recurent caculation.
'glyph_size' : 0,
'glyph_height' : 0,
'font_size' : 0,
'data' : []
}
# handle user meta-indication
if 'charset' in asset.meta:
if asset.meta['charset'] not in ['default', 'unicode']:
log.error(f"Unknown charset '{asset.meta['charset']}', abord")
return None
font_info['charset'] = asset.meta['charset']
if 'grid_size' not in asset.meta:
log.error("Missing critical grid size information, abord")
return None
grid_size = asset.meta['grid_size'].split('x')
font_info['grid_size_x'] = int(grid_size[0])
font_info['grid_size_y'] = int(grid_size[1])
if 'grid_padding' in asset.meta:
font_info['grid_padding'] = int(asset.meta['grid_padding'])
if 'grid_border' in asset.meta:
font_info['grid_border'] = int(asset.meta['grid_border'])
if 'proportional' in asset.meta:
font_info['is_proportional'] = asset.meta['proportional']
font_info['line_height'] = font_info['grid_size_y']
if 'line_height' in asset.meta:
font_info['line_height'] = asset.meta['line_height']
if 'char_spacing' in asset.meta:
font_info['char_spacing'] = asset.meta['char_spacing']
font_info['glyph_height'] = font_info['grid_size_y']
# return font information
return font_info
#---
# Internal glyph routines
#---
def __glyph_get_wgeometry(geometry_info, img_raw, img_size, pos, grid_size):
""" Generate glyph width geometry information
@args
> geometry_info (dict) - geometry information
> img_raw (list) - list of all pixel of the image
> img_size (tuple) - image width and image height
> pos (tuple) - glyph position information (X and Y in pixel)
> grid_size (tuple) - glyph grid size information (width and height)
@return
> Nothing
"""
geometry_info['wstart'] = -1
geometry_info['wend'] = -1
_px = pos[0]
_py = pos[1]
log.debug(f'[geometry] X:{pos[0]} Y:{int(pos[1]/img_size[0])}')
log.debug(f' - grid_size = {grid_size}')
for _ in range(0, grid_size[1]):
for offx in range(0, grid_size[0]):
if img_raw[_py + (_px + offx)][:3] == (255, 255, 255):
continue
if geometry_info['wstart'] < 0 or offx < geometry_info['wstart']:
geometry_info['wstart'] = offx
if geometry_info['wstart'] < 0 or offx > geometry_info['wend']:
geometry_info['wend'] = offx
_py += img_size[0]
geometry_info['wend'] += 1
log.debug(f' - geometry = {geometry_info}')
def __glyph_encode(data_info, img_info, geometry, posx, posy):
""" Encode glyph bitmap
@args
> data_info (dict) - internal data information (list, index and shift)
> img_info (dict) - image-related information (object and raw content)
> geometry (dict) - geometry information
> posx (int) - X-axis position in pixel
> posy (int) - Y-axis position in pixel
@return
> Nothing
"""
# fetch information
img = img_info['obj']
img_raw = img_info['raw']
data = data_info['table']
data_idx = data_info['idx']
data_shift = data_info['shift']
wstart = geometry['wstart']
wend = geometry['wend']
# encode the glyph
yoff = 0
log.debug(f'[encode] X:{posx} Y:{int(posy/img.size[0])}')
for _h in range(geometry['hstart'], geometry['hend']):
for _w in range(wstart, wend):
if img_raw[(posy + yoff) + (posx + _w)][:3] == (0, 0, 0):
log.debug('#', end='')
data[data_idx] |= 0x80000000 >> data_shift
else:
log.debug('.', end='')
data[data_idx] &= ~(0x80000000 >> data_shift)
if (data_shift := data_shift + 1) >= 32:
data_shift = 0
data_idx += 1
log.debug('')
yoff += img.size[0]
# commit modification
data_info['idx'] = data_idx
data_info['shift'] = data_shift
#---
# Intenal font conversion
#---
def __font_convert_proportional(packed_info):
""" Generate proportional font
Proportional font means that each character have its own width size (but
have a common height). We need to performs more complexe handling than the
monospaced one.
@args
> asset (VxAsset) - asset information
> font_information (dict) - font indication
@return
> 0 if success, negative value otherwise
"""
# unpack information
font_info = packed_info[0]
img_info = packed_info[1]
glyph_info = packed_info[2]
data_info = packed_info[4]
geometry_info = packed_info[5]
# isolate needed information
img = img_info['obj']
img_raw = img_info['raw']
nb_col = packed_info[3][0]
nb_row = packed_info[3][1]
gwidth = glyph_info[0]
gheight = glyph_info[1]
# main loop, walk glyph per glyph
_py = (font_info['grid_border'] + font_info['grid_padding']) * img.size[0]
for _ in range(0, nb_row):
_px = font_info['grid_border'] + font_info['grid_padding']
for _ in range(0, nb_col):
# generate width geometry information
__glyph_get_wgeometry(
geometry_info,
img_raw,
img.size,
(_px, _py),
(font_info['grid_size_x'], font_info['grid_size_y'])
)
# save critical glyph geometry information that will be encoded in
# the final C source file
font_info['glyph_props'].append((
geometry_info['wend'] - geometry_info['wstart'],
data_info['idx'],
data_info['shift']
))
# encode glyph information
__glyph_encode(data_info, img_info, geometry_info, _px, _py)
# update loop information
font_info['glyph_count'] += 1
_px += gwidth
_py += gheight * img.size[0]
return 0
def __font_convert_monospaced(packed_info):
""" Generate proportional font
Proportional font means that each character have its own width size (but
have a common height). We need to performs more complexe handling than the
monospaced one.
@args
> asset (VxAsset) - asset information
> font_information (dict) - font indication
@return
> 0 if success, negative value otherwise
"""
# unpack information
font_info = packed_info[0]
img_info = packed_info[1]
glyph_info = packed_info[2]
grid_info = packed_info[3]
data_info = packed_info[4]
geometry_info = packed_info[5]
# isolate needed information
img = img_info['obj']
nb_row = grid_info[1]
nb_col = grid_info[0]
gwidth = glyph_info[0]
gheight = glyph_info[1]
# main loop, walk glyph per glyph
_py = (font_info['grid_border'] + font_info['grid_padding']) * img.size[0]
for _ in range(0, nb_row):
_px = font_info['grid_border'] + font_info['grid_padding']
for _ in range(0, nb_col):
__glyph_encode(data_info, img_info, geometry_info, _px, _py)
font_info['glyph_count'] += 1
_px += gwidth
_py += gheight * img.size[0]
return 0
def __font_convert(asset, font_info):
""" Generate font information
@args
> asset (VxAsset) - asset information
> font_info (dict) - font information
@return
> 0 if success, negative value otherwise
"""
# generate image information
img = Image.open(asset.path)
img_raw = img.getdata()
img_info = {
'obj' : img,
'raw' : img_raw
}
# pre-calculate the "real" glyph width and height using padding information
glyph_info = [0, 0]
glyph_info[0] = font_info['grid_size_x'] + font_info['grid_padding']
glyph_info[1] = font_info['grid_size_y'] + font_info['grid_padding']
gheight = glyph_info[1]
gwidth = glyph_info[0]
log.debug(f"gwidth = {gwidth} && gheight = {gheight}")
# pre-calculate the number of row and column of the font
grid_info = [0, 0]
grid_info[0] = int((img.size[0] - (font_info['grid_border'] * 2)) / gwidth)
grid_info[1] = int((img.size[1] - (font_info['grid_border'] * 2)) / gheight)
nb_col = grid_info[0]
nb_row = grid_info[1]
log.debug(f"nb_row = {nb_row} && nb_col = {nb_col}")
# pre-calculate and prepare per-glyph information
# @note
# The generated data is designed for 4-alignement padding. This to have
# speed-up on drawing function.
font_info['glyph_size'] = font_info['grid_size_x'] * font_info['grid_size_y']
font_info['font_size'] = font_info['glyph_size'] * nb_row * nb_col
font_info['glyph_count'] = 0
font_info['glyph_props'] = []
font_info['data'] = [0] * int((font_info['font_size'] + 31) / 32)
log.debug(f"data original = {id(font_info['data'])}")
# generate data information
data_info = {
'table' : font_info['data'],
'idx' : 0,
'shift' : 0
}
log.debug(f"data packed = {id(data_info['table'])}")
# generate geometry information
geometry_info = {
'hstart' : 0,
'hend' : font_info['grid_size_y'],
'wstart' : 0,
'wend' : font_info['grid_size_x'],
}
# select the converter
converter = __font_convert_monospaced
if font_info['is_proportional']:
converter = __font_convert_proportional
# convert font
converter((
font_info,
img_info,
glyph_info,
grid_info,
data_info,
geometry_info
))
log.debug(f"data packed end = {id(data_info['table'])}")
return 0
#---
# Source file generation
#---
def __font_generate_unicode_source(_):
"""Unicode special chaset directory"""
log.error("unicode conversion not implemented yet o(x_x)o")
return ''
def __font_generate_normal_source(font_info):
"""Print chaset is a image file
"""
content = "\t.glyph = {\n"
content += f"\t\t.height = {font_info['glyph_height']},\n"
content += f"\t\t.line_height = {font_info['line_height']},\n"
# encode font bitmap
line = 0
log.debug(f"data = {font_info['data']}")
content += "\t\t.data = (uint32_t[]){\n"
for pixel in font_info['data']:
if line == 0:
content += '\t\t\t'
if line >= 1:
content += ' '
content += f"{pixel:#010x},"
if (line := line + 1) == 4:
content += '\n'
line = 0
if line != 0:
content += '\n'
content += '\t\t},\n'
# indicate the number of glyph in the bitmap
content += f"\t\t.count = {font_info['glyph_count']},\n"
# encode proportional information if needed
if font_info['is_proportional']:
content += '\t\t.prop = (struct __workaround[]){\n'
for prop in font_info['glyph_props']:
content += "\t\t\t{\n"
content += f"\t\t\t\t.width = {prop[0]},\n"
content += f"\t\t\t\t.index = {prop[1]},\n"
content += f"\t\t\t\t.shift = {prop[2]},\n"
content += "\t\t\t},\n"
else:
content += "\t\t.mono = {,\n"
content += f"\t\t\t.width = {font_info['glyph_width']},\n"
content += f"\t\t\t.size = {font_info['glyph_size']},\n"
content += "\t\t},\n"
content += "\t},\n"
# skip unicode struct
content += "\t.unicode = {\n"
content += "\t\t.blocks = NULL,\n"
content += "\t\t.block_count = 0,\n"
content += "\t}\n"
return content
def __font_generate_source_file(asset, font_info):
"""Generate font source file content
@args
> asset (VxAsset) - asset information
> info (dict) - hold font information
@return
> file C content string
"""
# generate basic header
content = "#include <vhex/display/font.h>\n"
content += "\n"
content += f"/* {asset.name} - Vhex asset\n"
content += " This object has been converted by using the vxSDK "
content += "converter */\n"
content += f"struct font const {asset.name} = " + "{\n"
content += f"\t.name = \"{asset.name}\",\n"
# shape information
content += "\t.shape = {\n"
content += "\t\t.bold = 0,\n"
content += "\t\t.italic = 0,\n"
content += "\t\t.serif = 0,\n"
content += "\t\t.mono = 0,\n"
content += f"\t\t.prop = {int(font_info['is_proportional'])},\n"
content += "\t},\n"
# manage display indication
content += f"\t.char_spacing = {font_info['char_spacing']},\n"
# handle special charset behaviour
if font_info['charset'] == 'unicode':
content += __font_generate_unicode_source(font_info)
else:
content += __font_generate_normal_source(font_info)
# closure and return
content += '};\n'
return content
#---
# Public
#---
def conv_font_generate(asset, prefix_output):
""" Convert an image asset to a C source file
@args
> asset (_VxAsset) - minimal asset information
> prefix_output (str) - prefix for source file generation
@return
> pathname of the generated file
"""
# generate font information
if not (font_info := __font_fetch_info(asset)):
return ''
if __font_convert(asset, font_info) != 0:
return ''
content = __font_generate_source_file(asset, font_info)
# create the source file
asset_src = f'{prefix_output}/{asset.name}_vxfont.c'
with open(asset_src, "w", encoding='utf8') as file:
file.write(content)
log.debug(f"source file generated at {asset_src}")
return asset_src