mirror of
https://git.planet-casio.com/Lephenixnoir/fxsdk.git
synced 2024-12-29 13:03:37 +01:00
fxconv: add support for {P4,P8}_{RGB565,RGB565A} for Azur
This commit is contained in:
parent
68be7fe522
commit
317b82348f
1 changed files with 168 additions and 72 deletions
240
fxconv/fxconv.py
240
fxconv/fxconv.py
|
@ -108,13 +108,22 @@ class CgProfile:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
CG_PROFILES = [
|
CG_PROFILES = [
|
||||||
# 16-bit R5G6B5
|
# 16-bit RGB565 and RGB565 with alpha
|
||||||
|
CgProfile(0x0, "rgb565", False),
|
||||||
|
CgProfile(0x1, "rgb565a", True),
|
||||||
|
# 8-bit palette for RGB565 and RGB565A (supported by Azur only)
|
||||||
|
CgProfile(0x4, "p8_rgb565", False),
|
||||||
|
CgProfile(0x5, "p8_rgb565a", True),
|
||||||
|
# 4-bit palette for RGB565 and RGB565A (supported by Azur only)
|
||||||
|
CgProfile(0x6, "p4_rgb565", False),
|
||||||
|
CgProfile(0x3, "p4_rgb565a", True),
|
||||||
|
|
||||||
|
# Original names for RGB565 and RGB565A
|
||||||
CgProfile(0x0, "r5g6b5", False),
|
CgProfile(0x0, "r5g6b5", False),
|
||||||
# 16-bit R5G6B5 with alpha
|
|
||||||
CgProfile(0x1, "r5g6b5a", True),
|
CgProfile(0x1, "r5g6b5a", True),
|
||||||
# 8-bit palette
|
# The original 8-bit palette mode of bopti (inferior to the other P8 modes)
|
||||||
CgProfile(0x2, "p8", True),
|
CgProfile(0x2, "p8", True),
|
||||||
# 4-bit palette
|
# The original 4-bit palette mode of bopti (same as P4_RGB565A)
|
||||||
CgProfile(0x3, "p4", True),
|
CgProfile(0x3, "p4", True),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -515,40 +524,51 @@ def convert_bopti_cg(input, params):
|
||||||
area = Area(params.get("area", {}), img)
|
area = Area(params.get("area", {}), img)
|
||||||
img = img.crop(area.tuple())
|
img = img.crop(area.tuple())
|
||||||
|
|
||||||
# If no profile is specified, fall back to r5g6b5 or r5g6b5a later on
|
# If no profile is specified, fall back to rgb565 or rgb565a later on
|
||||||
name = params.get("profile", None)
|
name = params.get("profile", None)
|
||||||
if name is not None:
|
if name is not None:
|
||||||
profile = CgProfile.find(name)
|
profile = CgProfile.find(name)
|
||||||
|
|
||||||
if name in [ "r5g6b5", "r5g6b5a", None ]:
|
if name in ["r5g6b5", "r5g6b5a", "rgb565", "rgb565a", None]:
|
||||||
# Encode the image into the 16-bit format
|
# Encode the image into the 16-bit format
|
||||||
encoded, alpha = r5g6b5(img)
|
encoded, alpha = r5g6b5(img)
|
||||||
|
|
||||||
name = "r5g6b5" if alpha is None else "r5g6b5a"
|
if name is None:
|
||||||
|
name = "rgb565" if alpha is None else "rgb565a"
|
||||||
profile = CgProfile.find(name)
|
profile = CgProfile.find(name)
|
||||||
|
|
||||||
elif name in [ "p4", "p8" ]:
|
elif name.startswith("p"):
|
||||||
|
if name in ["p8_rgb565", "p8_rgb565a"]:
|
||||||
|
trim_palette = True
|
||||||
|
palette_base = 0x80
|
||||||
|
else:
|
||||||
|
trim_palette = False
|
||||||
|
palette_base = 0x00
|
||||||
|
|
||||||
# Encoded the image into 16-bit with a palette of 16 or 256 entries
|
# Encoded the image into 16-bit with a palette of 16 or 256 entries
|
||||||
color_count = 1 << int(name[1])
|
color_count = 1 << int(name[1])
|
||||||
encoded, palette, alpha = r5g6b5(img, color_count=color_count)
|
encoded, palette, alpha = r5g6b5(img, color_count=color_count,
|
||||||
|
trim_palette=trim_palette, palette_base=palette_base)
|
||||||
|
|
||||||
|
color_count = len(palette) // 2
|
||||||
encoded = palette + encoded
|
encoded = palette + encoded
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise FxconvError(f"unknown color profile '{name}'")
|
raise FxconvError(f"unknown color profile '{name}'")
|
||||||
|
|
||||||
if alpha is not None and not profile.supports_alpha:
|
if alpha is not None and not profile.supports_alpha:
|
||||||
raise FxconvError(f"'{input}' has transparency; use r5g6b5a, p8 or p4")
|
raise FxconvError(f"'{input}' has transparency; use rgb565a, p8 or p4")
|
||||||
|
|
||||||
w, h, a = img.width, img.height, alpha or 0x0000
|
header = bytes()
|
||||||
|
header += u16(profile.id)
|
||||||
|
header += u16(alpha if alpha is not None else 0xffff)
|
||||||
|
header += u16(img.width) + u16(img.height)
|
||||||
|
|
||||||
header = bytearray([
|
if name in ["p8_rgb565", "p8_rgb565a"]:
|
||||||
0x00, profile.id, # Profile identification
|
header += u16(color_count)
|
||||||
a >> 8, a & 0xff, # Alpha color
|
|
||||||
w >> 8, w & 0xff, # Width
|
|
||||||
h >> 8, h & 0xff, # Height
|
|
||||||
])
|
|
||||||
|
|
||||||
|
if len(encoded) % 4 != 0:
|
||||||
|
encoded += bytes(4 - (len(encoded) % 4))
|
||||||
return header + encoded
|
return header + encoded
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -901,9 +921,9 @@ def quantize(img, dither=False):
|
||||||
|
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def r5g6b5(img, color_count=0, alpha=None):
|
def r5g6b5(img, color_count=0, trim_palette=False, palette_base=0, alpha=None):
|
||||||
"""
|
"""
|
||||||
Convert a PIL.Image.Image into an R5G6B5 byte stream. If there are
|
Convert a PIL.Image.Image into an RGB565 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
|
||||||
with this color.
|
with this color.
|
||||||
|
|
||||||
|
@ -915,18 +935,33 @@ def r5g6b5(img, color_count=0, alpha=None):
|
||||||
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 trim_palette is set, the palette bytearray is trimmed so that only used
|
||||||
|
entries are set. This option has no effect if color_count=0.
|
||||||
|
|
||||||
|
If palette_base is provided, palette entries will be numbered starting from
|
||||||
|
that value, wrapping around modulo color_count. If there is alpha, the
|
||||||
|
alpha value (which is always 0) is excluded from that cycle. This option
|
||||||
|
has no effect if color_count=0.
|
||||||
|
|
||||||
If alpha is provided, it should be a pair (alpha value, replacement).
|
If alpha is provided, it should be a pair (alpha value, replacement).
|
||||||
Trandarpent pixels will be encoded with the specified alpha value and
|
Transparent pixels will be encoded with the specified alpha value and
|
||||||
pixels with the value will be encoded with the replacement.
|
pixels with the value will be encoded with the replacement.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def rgb24to16(r, g, b):
|
def rgb24to16(rgb24):
|
||||||
|
r, g, b = rgb24
|
||||||
r = (r & 0xff) >> 3
|
r = (r & 0xff) >> 3
|
||||||
g = (g & 0xff) >> 2
|
g = (g & 0xff) >> 2
|
||||||
b = (b & 0xff) >> 3
|
b = (b & 0xff) >> 3
|
||||||
return (r << 11) | (g << 5) | b
|
return (r << 11) | (g << 5) | b
|
||||||
|
|
||||||
# Save the alpha channel and make it 1-bit
|
#---
|
||||||
|
# Initial image transforms
|
||||||
|
# Separate the alpha channel and generate a first palette.
|
||||||
|
#---
|
||||||
|
|
||||||
|
# Save the alpha channel and make it 1-bit. If there are transparent
|
||||||
|
# pixels, set has_alpha=True and record the alpha channel in alpha_pixels.
|
||||||
try:
|
try:
|
||||||
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() }
|
||||||
|
@ -942,54 +977,115 @@ def r5g6b5(img, color_count=0, alpha=None):
|
||||||
# Convert the input image to RGB
|
# Convert the input image to RGB
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
|
|
||||||
# Optionally convert to palette
|
# Transparent pixels also have values on the RGB channels, so they use up a
|
||||||
|
# palette entry (in indexed mode) of possible alpha value (in 16-bit mode)
|
||||||
|
# even though their color is unused. Replace them with a non-transparent
|
||||||
|
# color used elsewhere in the image to avoid that.
|
||||||
|
if has_alpha:
|
||||||
|
nontransparent_pixels = { (x,y)
|
||||||
|
for x in range(img.width) for y in range(img.height)
|
||||||
|
if alpha_pixels[x,y] > 0 }
|
||||||
|
|
||||||
|
if nontransparent_pixels:
|
||||||
|
x0, y0 = nontransparent_pixels.pop()
|
||||||
|
pixels = img.load()
|
||||||
|
|
||||||
|
for y in range(img.height):
|
||||||
|
for x in range(img.width):
|
||||||
|
if alpha_pixels[x,y] == 0:
|
||||||
|
pixels[x,y] = pixels[x0,y0]
|
||||||
|
|
||||||
|
# In indexed mode, generate a specific palette
|
||||||
if color_count:
|
if color_count:
|
||||||
palette_size = color_count - int(has_alpha)
|
palette_max_size = color_count - int(has_alpha)
|
||||||
img = img.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE,
|
img = img.convert("P",
|
||||||
colors=palette_size)
|
dither=Image.NONE,
|
||||||
palette = img.getpalette()
|
palette=Image.ADAPTIVE,
|
||||||
|
colors=palette_max_size)
|
||||||
|
|
||||||
|
# Format for the first palette is a list of N triplets where N is the
|
||||||
|
# number of used opaque colors; obviously N <= palette_max_size
|
||||||
|
palette1 = img.getpalette()[:3*len(img.getcolors())]
|
||||||
|
palette1 = list(zip(palette1[::3], palette1[1::3], palette1[2::3]))
|
||||||
|
|
||||||
pixels = img.load()
|
pixels = img.load()
|
||||||
|
|
||||||
# Choose an alpha color
|
#---
|
||||||
|
# Alpha encoding
|
||||||
|
# Find a value to encode transparency and map it into palettes.
|
||||||
|
#---
|
||||||
|
|
||||||
|
# RGB565A with fixed alpha value (fi. alpha=0x0001 in libimg)
|
||||||
if alpha is not None:
|
if alpha is not None:
|
||||||
alpha, replacement = alpha
|
if color_count > 0:
|
||||||
|
raise FxconvError("cannot choose alpha value in palette formats")
|
||||||
|
else:
|
||||||
|
alpha, replacement = alpha
|
||||||
|
|
||||||
elif color_count > 0:
|
# Alpha always uses color index 0 in palettes (helps faster rendering)
|
||||||
# Transparency is mapped to the last palette element, if there are no
|
elif color_count > 0 and has_alpha:
|
||||||
# transparent pixels then select an index out of bounds.
|
alpha = 0
|
||||||
alpha = color_count - 1 if has_alpha else 0xffff
|
|
||||||
|
|
||||||
|
# Find an unused RGB565 value and keep the encoding to 16-bit
|
||||||
elif has_alpha:
|
elif has_alpha:
|
||||||
# Compute the set of all used R5G6B5 colors
|
colormap = { rgb24to16(pixels[x, y])
|
||||||
colormap = set()
|
for x in range(img.width) for y in range(img.height)
|
||||||
|
if alpha_pixels[x, y] > 0 }
|
||||||
|
|
||||||
for y in range(img.height):
|
|
||||||
for x in range(img.width):
|
|
||||||
if alpha_pixels[x, y] > 0:
|
|
||||||
colormap.add(rgb24to16(*pixels[x, y]))
|
|
||||||
|
|
||||||
# Choose an alpha color among the unused ones
|
|
||||||
available = set(range(65536)) - colormap
|
available = set(range(65536)) - colormap
|
||||||
|
|
||||||
if not available:
|
if not available:
|
||||||
raise FxconvError("image uses all 65536 colors and alpha")
|
raise FxconvError("image uses all 65536 colors and alpha")
|
||||||
alpha = available.pop()
|
alpha = available.pop()
|
||||||
|
|
||||||
|
# No transparency in the image
|
||||||
else:
|
else:
|
||||||
alpha = None
|
alpha = None
|
||||||
|
|
||||||
|
# Function to encode with alpha support in RGB565
|
||||||
def alpha_encoding(color, a):
|
def alpha_encoding(color, a):
|
||||||
if a > 0:
|
if a > 0: # pixel is not transparent
|
||||||
if color == alpha:
|
return color if color != alpha else replacement
|
||||||
return replacement
|
|
||||||
else:
|
|
||||||
return color
|
|
||||||
else:
|
else:
|
||||||
return alpha
|
return alpha
|
||||||
|
|
||||||
# Create a byte array with all encoded pixels
|
# In palette formats, rearrange the palette to account for palette_base,
|
||||||
|
# insert alpha, and determine encoded size (which may include alpha)
|
||||||
|
|
||||||
|
if color_count > 0:
|
||||||
|
# The palette remap indicates how to transform indices of the first
|
||||||
|
# palette into (1) signed or unsigned indices starting at palette_base,
|
||||||
|
# and (2) indices into the physically encoded palette (always starting
|
||||||
|
# at 0). Each entry is a tuple with both values.
|
||||||
|
palette_remap = [(-1,-1)] * len(palette1)
|
||||||
|
passed_alpha = False
|
||||||
|
|
||||||
|
index1 = palette_base
|
||||||
|
index2 = 0
|
||||||
|
|
||||||
|
for i in range(len(palette1)):
|
||||||
|
# Leave an empty spot for the alpha value
|
||||||
|
if index1 == alpha:
|
||||||
|
index1 += 1
|
||||||
|
index2 += 1
|
||||||
|
passed_alpha = True
|
||||||
|
|
||||||
|
palette_remap[i] = (index1, index2)
|
||||||
|
|
||||||
|
index1 += 1
|
||||||
|
index2 += 1
|
||||||
|
if index1 >= color_count:
|
||||||
|
index1 = 0
|
||||||
|
|
||||||
|
# How many entries are needed in the palette (for trim_palette). This
|
||||||
|
# is either len(palette1) or len(palette1) + 1 depending on whether the
|
||||||
|
# alpha value stands in the middle
|
||||||
|
palette_bin_size = len(palette1) + passed_alpha
|
||||||
|
|
||||||
|
#---
|
||||||
|
# Image encoding
|
||||||
|
# Create byte arrays with pixel data and palette data
|
||||||
|
#---
|
||||||
|
|
||||||
pixel_count = img.width * img.height
|
pixel_count = img.width * img.height
|
||||||
|
|
||||||
|
@ -998,58 +1094,58 @@ def r5g6b5(img, color_count=0, alpha=None):
|
||||||
elif color_count == 256:
|
elif color_count == 256:
|
||||||
size = pixel_count
|
size = pixel_count
|
||||||
elif color_count == 16:
|
elif color_count == 16:
|
||||||
size = (pixel_count + 1) // 2
|
size = ((img.width + 1) // 2) * img.height
|
||||||
|
|
||||||
# Result of encoding
|
# Result of encoding
|
||||||
encoded = bytearray(size)
|
encoded = bytearray(size)
|
||||||
# Number of pixels encoded so far
|
|
||||||
entries = 0
|
|
||||||
# Offset into the array
|
# Offset into the array
|
||||||
offset = 0
|
offset = 0
|
||||||
|
|
||||||
for y in range(img.height):
|
for y in range(img.height):
|
||||||
for x in range(img.width):
|
for x in range(img.width):
|
||||||
a = alpha_pixels[x, y] if has_alpha else 0xff
|
a = alpha_pixels[x, y] if has_alpha else 255
|
||||||
|
|
||||||
if not color_count:
|
if not color_count:
|
||||||
c = alpha_encoding(rgb24to16(*pixels[x, y]), a)
|
c = alpha_encoding(rgb24to16(pixels[x, y]), a)
|
||||||
encoded[offset] = c >> 8
|
encoded[offset:offset+2] = u16(c)
|
||||||
encoded[offset+1] = c & 0xff
|
|
||||||
offset += 2
|
offset += 2
|
||||||
|
|
||||||
elif color_count == 16:
|
elif color_count == 16:
|
||||||
c = alpha_encoding(pixels[x, y], a)
|
c = palette_remap[pixels[x, y]][0] if a > 0 else alpha
|
||||||
|
|
||||||
# Aligned pixels: left 4 bits = high 4 bits of current byte
|
# Select either the 4 MSb or 4 LSb of the current byte
|
||||||
if (entries % 2) == 0:
|
if x % 2 == 0:
|
||||||
encoded[offset] |= (c << 4)
|
encoded[offset] |= (c << 4)
|
||||||
# Unaligned pixels: right 4 bits of current byte
|
|
||||||
else:
|
else:
|
||||||
encoded[offset] |= c
|
encoded[offset] |= c
|
||||||
offset += 1
|
|
||||||
|
offset += (x % 2 == 1) or (x == img.width - 1)
|
||||||
|
|
||||||
elif color_count == 256:
|
elif color_count == 256:
|
||||||
c = alpha_encoding(pixels[x, y], a)
|
c = palette_remap[pixels[x, y]][0] if a > 0 else alpha
|
||||||
encoded[offset] = c
|
encoded[offset] = c
|
||||||
offset += 1
|
offset += 1
|
||||||
|
|
||||||
entries += 1
|
|
||||||
|
|
||||||
if not color_count:
|
|
||||||
return encoded, alpha
|
|
||||||
|
|
||||||
# Encode the palette as R5G6B5
|
# Encode the palette as R5G6B5
|
||||||
|
|
||||||
encoded_palette = bytearray(2 * color_count)
|
if color_count > 0:
|
||||||
|
if trim_palette:
|
||||||
|
encoded_palette = bytearray(2 * palette_bin_size)
|
||||||
|
else:
|
||||||
|
encoded_palette = bytearray(2 * color_count)
|
||||||
|
|
||||||
for c in range(color_count - int(has_alpha)):
|
for i in range(len(palette1)):
|
||||||
r, g, b = palette[3*c], palette[3*c+1], palette[3*c+2]
|
index = palette_remap[i][1]
|
||||||
rgb16 = rgb24to16(r, g, b)
|
encoded_palette[2*index:2*index+2] = u16(rgb24to16(palette1[i]))
|
||||||
|
|
||||||
encoded_palette[2*c] = rgb16 >> 8
|
#---
|
||||||
encoded_palette[2*c+1] = rgb16 & 0xff
|
# Outro
|
||||||
|
#---
|
||||||
|
|
||||||
return encoded, encoded_palette, alpha
|
if color_count > 0:
|
||||||
|
return encoded, encoded_palette, alpha
|
||||||
|
else:
|
||||||
|
return encoded, alpha
|
||||||
|
|
||||||
def convert(input, params, target, output=None, model=None, custom=None):
|
def convert(input, params, target, output=None, model=None, custom=None):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue