""" 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 \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