fxconv: add suport for libimg images and deprecate --image

This commit introduces the libimg image format, selected with the option
type:libimg-image. To avoid confusion with the bopti image format,
options -i and --image are now deprecated and should be replaced with
--bopti-image or type:bopti-image. The fxSDK Makefile has been updated
accordingly.

To support the construction of a structure that contains a pointer in
fxconv, an assembly-code feature has been added. The structure itself is
assembled with as and then linked with the data proper. This allows the
structure to reference the data by name and have the pointer calculated
by ld at link time.
This commit is contained in:
Lephenixnoir 2020-03-11 19:37:27 +01:00
parent b86b96aa4a
commit c79b3b1a9d
No known key found for this signature in database
GPG key ID: 1BBA026E13FC0495
3 changed files with 132 additions and 35 deletions

View file

@ -17,15 +17,16 @@ optimized for fast execution, or into object files.
Operating modes: Operating modes:
-s, --script Expose the fxconv module and run this Python script -s, --script Expose the fxconv module and run this Python script
-b, --binary Turn data into an object file, no conversion -b, --binary Turn data into an object file, no conversion
-i, --image Convert to gint's image format -i, --image Convert to gint's bopti image format
-f, --font Convert to gint's font format -f, --font Convert to gint's topti font format
--libimg-image Convert to the libimg image format
When using -s, additional arguments are stored in the [fxconv.args] variable of When using -s, additional arguments are stored in the [fxconv.args] variable of
the module. This is intended to be a restricted list of file names specified by the module. This is intended to be a restricted list of file names specified by
a Makefile, used to convert only a subset of the files in the script. a Makefile, used to convert only a subset of the files in the script.
The -b, -i and -f modes are shortcuts to convert single files without a script. The operating mode options are shortcuts to convert single files without a
They accept parameters with a "category.key:value" syntax, for example: script. They accept parameters with a "category.key:value" syntax, for example:
fxconv -f myfont.png -o myfont.o charset:ascii grid.padding:1 height:7 fxconv -f myfont.png -o myfont.o charset:ascii grid.padding:1 height:7
When converting images, use --fx (black-and-white calculators) or --cg (16-bit When converting images, use --fx (black-and-white calculators) or --cg (16-bit
@ -36,11 +37,11 @@ color calculators) to specify the target machine.
def err(msg): def err(msg):
print("\x1b[31;1merror:\x1b[0m", msg, file=sys.stderr) print("\x1b[31;1merror:\x1b[0m", msg, file=sys.stderr)
def warn(msg): def warn(msg):
print("warning:", msg, file=sys.stderr) print("\x1b[33;1mwarning:\x1b[0m", msg, file=sys.stderr)
def main(): def main():
# Default execution mode is to run a Python script for conversion # Default execution mode is to run a Python script for conversion
modes = "script binary image font" modes = "script binary image font bopti-image libimg-image"
mode = "s" mode = "s"
output = None output = None
model = None model = None
@ -79,7 +80,7 @@ def main():
target['section'] = value target['section'] = value
# Other names are modes # Other names are modes
else: else:
mode = name[1] if len(name)==2 else name[2] mode = name[1] if len(name)==2 else name[2:]
# Remaining arguments # Remaining arguments
if args == []: if args == []:
@ -118,7 +119,17 @@ def main():
for (name, value) in args: for (name, value) in args:
insert(params, name.split("."), value) insert(params, name.split("."), value)
if "type" in params:
pass
elif(len(mode) == 1):
params["type"] = { "b": "binary", "i": "image", "f": "font" }[mode] params["type"] = { "b": "binary", "i": "image", "f": "font" }[mode]
else:
params["type"] = mode
# Will be deprecated in the future
if params["type"] == "image":
warn("type 'image' is deprecated, use 'bopti-image' instead")
params["type"] = "bopti-image"
try: try:
fxconv.convert(input, params, target, output, model) fxconv.convert(input, params, target, output, model)

View file

@ -503,6 +503,42 @@ def convert_topti(input, output, params, target):
elf(data, output, "_" + params["name"], **target) elf(data, output, "_" + params["name"], **target)
#
# libimg conversion for fx-CG 50
#
def convert_libimg_cg(input, output, params, target):
img = Image.open(input)
if img.width >= 65536 or img.height >= 65536:
raise FxconvError(f"'{input}' is too large (max. 65535x65535)")
# Crop image to key "area"
area = Area(params.get("area", {}), img)
img = img.crop(area.tuple())
# Encode the image into 16-bit format and force the alpha to 0x0001
encoded, alpha = r5g6b5(img, alpha=(0x0001,0x0000))
w, h, s = img.width, img.height, img.width
FLAG_OWN = 1
FLAG_RO = 2
assembly = f"""
.section .rodata
.global _{params["name"]}
_{params["name"]}:
.word {img.width}
.word {img.height}
.word {img.width}
.byte {FLAG_RO}
.byte 0
.long _{params["name"]}_data
"""
dataname = "_{}_data".format(params["name"])
elf(encoded, output, dataname, assembly=assembly, **target)
# #
# Exceptions # Exceptions
# #
@ -575,7 +611,7 @@ def quantize(img, dither=False):
return img return img
def r5g6b5(img, color_count=0): def r5g6b5(img, color_count=0, alpha=None):
""" """
Convert a PIL.Image.Image into an R5G6B5 byte stream. If there are Convert a PIL.Image.Image into an R5G6B5 byte stream. If there are
transparent pixels, chooses a color to implement alpha and replaces them transparent pixels, chooses a color to implement alpha and replaces them
@ -588,6 +624,10 @@ def r5g6b5(img, color_count=0):
encoded with a palette of this size. Returns the converted image as a encoded with a palette of this size. Returns the converted image as a
bytearray, the palette as a bytearray, and the alpha value (None if there bytearray, the palette as a bytearray, and the alpha value (None if there
were no transparent pixels). were no transparent pixels).
If alpha is provided, it should be a pair (alpha value, replacement).
Trandarpent pixels will be encoded with the specified alpha value and
pixels with the value will be encoded with the replacement.
""" """
def rgb24to16(r, g, b): def rgb24to16(r, g, b):
@ -601,6 +641,7 @@ def r5g6b5(img, color_count=0):
alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE) alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE)
alpha_levels = { t[1]: t[0] for t in alpha_channel.getcolors() } alpha_levels = { t[1]: t[0] for t in alpha_channel.getcolors() }
has_alpha = 0 in alpha_levels has_alpha = 0 in alpha_levels
replacement = None
if has_alpha: if has_alpha:
alpha_pixels = alpha_channel.load() alpha_pixels = alpha_channel.load()
@ -622,7 +663,10 @@ def r5g6b5(img, color_count=0):
# Choose an alpha color # Choose an alpha color
if color_count > 0: if alpha is not None:
alpha, replacement = alpha
elif color_count > 0:
# Transparency is mapped to the last palette element, if there are no # Transparency is mapped to the last palette element, if there are no
# transparent pixels then select an index out of bounds. # transparent pixels then select an index out of bounds.
alpha = color_count - 1 if has_alpha else 0xffff alpha = color_count - 1 if has_alpha else 0xffff
@ -646,6 +690,15 @@ def r5g6b5(img, color_count=0):
else: else:
alpha = None alpha = None
def alpha_encoding(color, a):
if a > 0:
if color == alpha:
return replacement
else:
return color
else:
return alpha
# Create a byte array with all encoded pixels # Create a byte array with all encoded pixels
pixel_count = img.width * img.height pixel_count = img.width * img.height
@ -669,13 +722,13 @@ def r5g6b5(img, color_count=0):
a = alpha_pixels[x, y] if has_alpha else 0xff a = alpha_pixels[x, y] if has_alpha else 0xff
if not color_count: if not color_count:
c = rgb24to16(*pixels[x, y]) if a > 0 else alpha c = alpha_encoding(rgb24to16(*pixels[x, y]), a)
encoded[offset] = c >> 8 encoded[offset] = c >> 8
encoded[offset+1] = c & 0xff encoded[offset+1] = c & 0xff
offset += 2 offset += 2
elif color_count == 16: elif color_count == 16:
c = pixels[x, y] if a > 0 else alpha c = alpha_encoding(pixels[x, y], a)
# Aligned pixels: left 4 bits = high 4 bits of current byte # Aligned pixels: left 4 bits = high 4 bits of current byte
if (entries % 2) == 0: if (entries % 2) == 0:
@ -686,7 +739,7 @@ def r5g6b5(img, color_count=0):
offset += 1 offset += 1
elif color_count == 256: elif color_count == 256:
c = pixels[x, y] if a > 0 else alpha c = alpha_encoding(pixels[x, y], a)
encoded[offset] = c encoded[offset] = c
offset += 1 offset += 1
@ -739,14 +792,21 @@ def convert(input, params, target, output=None, model=None):
raise FxconvError(f"missing type in conversion '{input}'") raise FxconvError(f"missing type in conversion '{input}'")
elif params["type"] == "binary": elif params["type"] == "binary":
convert_binary(input, output, params, target) convert_binary(input, output, params, target)
elif params["type"] == "image" and model in [ "fx", None ]: elif params["type"] == "bopti-image" and model in [ "fx", None ]:
convert_bopti_fx(input, output, params, target) convert_bopti_fx(input, output, params, target)
elif params["type"] == "image" and model == "cg": elif params["type"] == "bopti-image" and model == "cg":
convert_bopti_cg(input, output, params, target) convert_bopti_cg(input, output, params, target)
elif params["type"] == "font": elif params["type"] == "font":
convert_topti(input, output, params, target) convert_topti(input, output, params, target)
elif params["type"] == "libimg-image" and model in [ "fx", None ]:
raise FxconvError(f"libimg not yet supported for fx-9860G o(x_x)o")
elif params["type"] == "libimg-image" and model == "cg":
convert_libimg_cg(input, output, params, target)
else:
raise FxconvError(f'unknown resource type \'{params["type"]}\'')
def elf(data, output, symbol, toolchain=None, arch=None, section=None): def elf(data, output, symbol, toolchain=None, arch=None, section=None,
assembly=None):
""" """
Call objcopy to create an object file from the specified data. The object Call objcopy to create an object file from the specified data. The object
file will export three symbols: file will export three symbols:
@ -773,6 +833,10 @@ def elf(data, output, symbol, toolchain=None, arch=None, section=None):
would be section=".rodata,contents,alloc,load,readonly,data", which is the would be section=".rodata,contents,alloc,load,readonly,data", which is the
default. default.
If assembly is set to a non-empty assembly program, this function also
generates a temporary ELF file by assembling this piece of code, and merges
it into the original one.
Arguments: Arguments:
data -- A bytes-like object with data to embed into the object file data -- A bytes-like object with data to embed into the object file
output -- Name of output file output -- Name of output file
@ -780,6 +844,7 @@ def elf(data, output, symbol, toolchain=None, arch=None, section=None):
toolchain -- Target triplet [default: "sh3eb-elf"] toolchain -- Target triplet [default: "sh3eb-elf"]
arch -- Target architecture [default: try to guess] arch -- Target architecture [default: try to guess]
section -- Target section [default: above variation of .rodata] section -- Target section [default: above variation of .rodata]
assembly -- Additional assembly code [default: None]
Produces an output file and returns nothing. Produces an output file and returns nothing.
""" """
@ -802,11 +867,21 @@ def elf(data, output, symbol, toolchain=None, arch=None, section=None):
raise FxconvError(f"non-trivial architecture for {toolchain} must be "+ raise FxconvError(f"non-trivial architecture for {toolchain} must be "+
"specified") "specified")
with tempfile.NamedTemporaryFile() as fp: fp_obj = tempfile.NamedTemporaryFile()
fp.write(data) fp_obj.write(data)
fp.flush() fp_obj.flush()
sybl = "_binary_" + fp.name.replace("/", "_") if assembly is not None:
fp_asm = tempfile.NamedTemporaryFile()
fp_asm.write(assembly.encode('utf-8'))
fp_asm.flush()
proc = subprocess.run([
f"{toolchain}-as", "-c", fp_asm.name, "-o", fp_asm.name + ".o" ])
if proc.returncode != 0:
raise FxconvError(f"as returned {proc.returncode}")
sybl = "_binary_" + fp_obj.name.replace("/", "_")
objcopy_args = [ objcopy_args = [
f"{toolchain}-objcopy", "-I", "binary", "-O", "elf32-sh", f"{toolchain}-objcopy", "-I", "binary", "-O", "elf32-sh",
@ -815,8 +890,19 @@ def elf(data, output, symbol, toolchain=None, arch=None, section=None):
"--redefine-sym", f"{sybl}_start={symbol}", "--redefine-sym", f"{sybl}_start={symbol}",
"--redefine-sym", f"{sybl}_end={symbol}_end", "--redefine-sym", f"{sybl}_end={symbol}_end",
"--redefine-sym", f"{sybl}_size={symbol}_size", "--redefine-sym", f"{sybl}_size={symbol}_size",
fp.name, output ] fp_obj.name, output if assembly is None else fp_obj.name + "-tmp" ]
proc = subprocess.run(objcopy_args) proc = subprocess.run(objcopy_args)
if proc.returncode != 0: if proc.returncode != 0:
raise FxconvError(f"objcopy returned {proc.returncode}") raise FxconvError(f"objcopy returned {proc.returncode}")
if assembly is not None:
proc = subprocess.run([
f"{toolchain}-ld", "-r", fp_obj.name + "-tmp", fp_asm.name + ".o",
"-o", output ])
if proc.returncode != 0:
raise FxconvError("ld returned {proc.returncode}")
fp_asm.close()
fp_obj.close()

View file

@ -136,10 +136,10 @@ build-cg/%.S.o: %.S
# Images # Images
build-fx/assets/img/%.o: assets-fx/img/% build-fx/assets/img/%.o: assets-fx/img/%
@ mkdir -p $(dir $@) @ mkdir -p $(dir $@)
fxconv -i $< -o $@ $(FXCONVFX) name:img_$(basename $*) $(IMG.$*) fxconv --bopti-image $< -o $@ $(FXCONVFX) name:img_$(basename $*) $(IMG.$*)
build-cg/assets/img/%.o: assets-cg/img/% build-cg/assets/img/%.o: assets-cg/img/%
@ mkdir -p $(dir $@) @ mkdir -p $(dir $@)
fxconv -i $< -o $@ $(FXCONVCG) name:img_$(basename $*) $(IMG.$*) fxconv --bopti-image $< -o $@ $(FXCONVCG) name:img_$(basename $*) $(IMG.$*)
# Fonts # Fonts
build-fx/assets/fonts/%.o: assets-fx/fonts/% build-fx/assets/fonts/%.o: assets-fx/fonts/%