Browse Source

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.
pull/4/head
Lephenixnoir 5 months ago
parent
commit
c79b3b1a9d
Signed by: Lephenixnoir <sebastien.michelland@protonmail.com> GPG Key ID: 1BBA026E13FC0495
3 changed files with 132 additions and 35 deletions
  1. +21
    -10
      fxconv/fxconv-main.py
  2. +109
    -23
      fxconv/fxconv.py
  3. +2
    -2
      fxsdk/assets/Makefile

+ 21
- 10
fxconv/fxconv-main.py View File

@@ -15,17 +15,18 @@ fxconv converts data files such as images and fonts into gint formats
optimized for fast execution, or into object files.

Operating modes:
-s, --script Expose the fxconv module and run this Python script
-b, --binary Turn data into an object file, no conversion
-i, --image Convert to gint's image format
-f, --font Convert to gint's font format
-s, --script Expose the fxconv module and run this Python script
-b, --binary Turn data into an object file, no conversion
-i, --image Convert to gint's bopti image 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
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.

The -b, -i and -f modes are shortcuts to convert single files without a script.
They accept parameters with a "category.key:value" syntax, for example:
The operating mode options are shortcuts to convert single files without a
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

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):
print("\x1b[31;1merror:\x1b[0m", msg, file=sys.stderr)
def warn(msg):
print("warning:", msg, file=sys.stderr)
print("\x1b[33;1mwarning:\x1b[0m", msg, file=sys.stderr)

def main():
# 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"
output = None
model = None
@@ -79,7 +80,7 @@ def main():
target['section'] = value
# Other names are modes
else:
mode = name[1] if len(name)==2 else name[2]
mode = name[1] if len(name)==2 else name[2:]

# Remaining arguments
if args == []:
@@ -118,7 +119,17 @@ def main():
for (name, value) in args:
insert(params, name.split("."), value)

params["type"] = { "b": "binary", "i": "image", "f": "font" }[mode]
if "type" in params:
pass
elif(len(mode) == 1):
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:
fxconv.convert(input, params, target, output, model)


+ 109
- 23
fxconv/fxconv.py View File

@@ -503,6 +503,42 @@ def convert_topti(input, output, params, 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
#
@@ -575,7 +611,7 @@ def quantize(img, dither=False):

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
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
bytearray, the palette as a bytearray, and the alpha value (None if there
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):
@@ -601,6 +641,7 @@ def r5g6b5(img, color_count=0):
alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE)
alpha_levels = { t[1]: t[0] for t in alpha_channel.getcolors() }
has_alpha = 0 in alpha_levels
replacement = None

if has_alpha:
alpha_pixels = alpha_channel.load()
@@ -622,7 +663,10 @@ def r5g6b5(img, color_count=0):

# 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
# transparent pixels then select an index out of bounds.
alpha = color_count - 1 if has_alpha else 0xffff
@@ -646,6 +690,15 @@ def r5g6b5(img, color_count=0):
else:
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

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

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+1] = c & 0xff
offset += 2

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
if (entries % 2) == 0:
@@ -686,7 +739,7 @@ def r5g6b5(img, color_count=0):
offset += 1

elif color_count == 256:
c = pixels[x, y] if a > 0 else alpha
c = alpha_encoding(pixels[x, y], a)
encoded[offset] = c
offset += 1

@@ -739,14 +792,21 @@ def convert(input, params, target, output=None, model=None):
raise FxconvError(f"missing type in conversion '{input}'")
elif params["type"] == "binary":
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)
elif params["type"] == "image" and model == "cg":
elif params["type"] == "bopti-image" and model == "cg":
convert_bopti_cg(input, output, params, target)
elif params["type"] == "font":
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
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
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:
data -- A bytes-like object with data to embed into the object 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"]
arch -- Target architecture [default: try to guess]
section -- Target section [default: above variation of .rodata]
assembly -- Additional assembly code [default: None]

Produces an output file and returns nothing.
"""
@@ -802,21 +867,42 @@ def elf(data, output, symbol, toolchain=None, arch=None, section=None):
raise FxconvError(f"non-trivial architecture for {toolchain} must be "+
"specified")

with tempfile.NamedTemporaryFile() as fp:
fp.write(data)
fp.flush()

sybl = "_binary_" + fp.name.replace("/", "_")
fp_obj = tempfile.NamedTemporaryFile()
fp_obj.write(data)
fp_obj.flush()

objcopy_args = [
f"{toolchain}-objcopy", "-I", "binary", "-O", "elf32-sh",
"--binary-architecture", arch, "--file-alignment", "4",
"--rename-section", f".data={section}",
"--redefine-sym", f"{sybl}_start={symbol}",
"--redefine-sym", f"{sybl}_end={symbol}_end",
"--redefine-sym", f"{sybl}_size={symbol}_size",
fp.name, output ]
if assembly is not None:
fp_asm = tempfile.NamedTemporaryFile()
fp_asm.write(assembly.encode('utf-8'))
fp_asm.flush()

proc = subprocess.run(objcopy_args)
proc = subprocess.run([
f"{toolchain}-as", "-c", fp_asm.name, "-o", fp_asm.name + ".o" ])
if proc.returncode != 0:
raise FxconvError(f"objcopy returned {proc.returncode}")
raise FxconvError(f"as returned {proc.returncode}")

sybl = "_binary_" + fp_obj.name.replace("/", "_")

objcopy_args = [
f"{toolchain}-objcopy", "-I", "binary", "-O", "elf32-sh",
"--binary-architecture", arch, "--file-alignment", "4",
"--rename-section", f".data={section}",
"--redefine-sym", f"{sybl}_start={symbol}",
"--redefine-sym", f"{sybl}_end={symbol}_end",
"--redefine-sym", f"{sybl}_size={symbol}_size",
fp_obj.name, output if assembly is None else fp_obj.name + "-tmp" ]

proc = subprocess.run(objcopy_args)
if proc.returncode != 0:
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()

+ 2
- 2
fxsdk/assets/Makefile View File

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

# Fonts
build-fx/assets/fonts/%.o: assets-fx/fonts/%


Loading…
Cancel
Save