mirror of
https://git.planet-casio.com/Lephenixnoir/fxsdk.git
synced 2024-12-28 04:23:37 +01:00
fxconv: rewrite image converter, forcing alpha value
This commit is contained in:
parent
58cb14157d
commit
b29c494715
1 changed files with 161 additions and 235 deletions
396
fxconv/fxconv.py
396
fxconv/fxconv.py
|
@ -90,40 +90,50 @@ FX_PROFILES = [
|
|||
|
||||
# fx-CG 50 profiles
|
||||
class CgProfile:
|
||||
def __init__(self, id, name, alpha):
|
||||
"""
|
||||
Construct a CgProfile object.
|
||||
* [id] is the profile ID in bopti
|
||||
* [name] is the profile name as found in the specification key
|
||||
* [alpha] is True if this profile supports alpha, False otherwise
|
||||
"""
|
||||
|
||||
def __init__(self, id, depth, names, alpha=None, palette=None):
|
||||
# Numerical ID
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.supports_alpha = alpha
|
||||
# Name for printing
|
||||
self.names = names
|
||||
# Bit depth
|
||||
self.depth = depth
|
||||
# Whether the profile has alpha
|
||||
self.has_alpha = (alpha is not None)
|
||||
# Alpha value (None has_alpha == False)
|
||||
self.alpha = alpha
|
||||
# Whether the profile is indexed
|
||||
self.is_indexed = (palette is not None)
|
||||
|
||||
if palette is not None:
|
||||
# Palette base value (skipping the alpha value)
|
||||
self.palette_base = palette[0]
|
||||
# Color count (indices are always 0..color_count-1, wraps around)
|
||||
self.color_count = palette[1]
|
||||
# Whether to trim the palette to a minimal length
|
||||
self.trim_palette = palette[2]
|
||||
|
||||
@staticmethod
|
||||
def find(name):
|
||||
"""Find a profile by name."""
|
||||
for profile in CG_PROFILES:
|
||||
if profile.name == name:
|
||||
if name in profile.names:
|
||||
return profile
|
||||
return None
|
||||
|
||||
IMAGE_RGB16 = 0
|
||||
IMAGE_P8 = 1
|
||||
IMAGE_P4 = 2
|
||||
|
||||
CG_PROFILES = [
|
||||
# 16-bit RGB565 and RGB565 with alpha
|
||||
CgProfile(0x0, "rgb565", False),
|
||||
CgProfile(0x1, "rgb565a", True),
|
||||
CgProfile(0x0, IMAGE_RGB16, ["rgb565", "r5g6b5"]),
|
||||
CgProfile(0x1, IMAGE_RGB16, ["rgb565a", "r5g6b5a"], alpha=0x0001),
|
||||
# 8-bit palette for RGB565 and RGB565A
|
||||
CgProfile(0x4, "p8_rgb565", False),
|
||||
CgProfile(0x5, "p8_rgb565a", True),
|
||||
CgProfile(0x4, IMAGE_P8, "p8_rgb565", palette=(0x80,256,True)),
|
||||
CgProfile(0x5, IMAGE_P8, "p8_rgb565a", alpha=0x80, palette=(0x81,256,True)),
|
||||
# 4-bit palette for RGB565 and RGB565A
|
||||
CgProfile(0x6, "p4_rgb565", False),
|
||||
CgProfile(0x3, "p4_rgb565a", True),
|
||||
|
||||
# Original names for RGB565 and RGB565A
|
||||
CgProfile(0x0, "r5g6b5", False),
|
||||
CgProfile(0x1, "r5g6b5a", True),
|
||||
CgProfile(0x6, IMAGE_P4, "p4_rgb565", palette=(0,16,False)),
|
||||
CgProfile(0x3, IMAGE_P4, "p4_rgb565a", alpha=0, palette=(1,16,False)),
|
||||
]
|
||||
|
||||
# Libimg flags
|
||||
|
@ -511,7 +521,19 @@ def _image_project(img, f):
|
|||
# Image conversion for fx-CG 50
|
||||
#
|
||||
|
||||
def image_has_alpha(img):
|
||||
# Convert the alpha channel to 1-bit; check if there are transparent pixels
|
||||
try:
|
||||
alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE)
|
||||
alpha_levels = { t[1]: t[0] for t in alpha_channel.getcolors() }
|
||||
return 0 in alpha_levels
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def convert_bopti_cg(input, params):
|
||||
return convert_image_cg(input, params)
|
||||
|
||||
def convert_image_cg(input, params):
|
||||
if isinstance(input, Image.Image):
|
||||
img = input.copy()
|
||||
else:
|
||||
|
@ -523,68 +545,48 @@ def convert_bopti_cg(input, params):
|
|||
area = Area(params.get("area", {}), img)
|
||||
img = img.crop(area.tuple())
|
||||
|
||||
# If no profile is specified, fall back to rgb565 or rgb565a later on
|
||||
name = params.get("profile", None)
|
||||
if name is not None:
|
||||
profile = CgProfile.find(name)
|
||||
img = img.convert("RGBA")
|
||||
|
||||
if name in ["r5g6b5", "r5g6b5a", "rgb565", "rgb565a", None]:
|
||||
# Encode the image into the 16-bit format
|
||||
encoded, stride, alpha = r5g6b5(img)
|
||||
#---
|
||||
# Format selection
|
||||
#---
|
||||
|
||||
if name is None:
|
||||
name = "rgb565" if alpha is None else "rgb565a"
|
||||
profile = CgProfile.find(name)
|
||||
format_name = params.get("profile", "")
|
||||
has_alpha = image_has_alpha(img)
|
||||
|
||||
color_count = -1
|
||||
palette = None
|
||||
# If no format is specified, select rgb565 or rgb565a
|
||||
if format_name == "":
|
||||
format_name = "rgb565a" if has_alpha else "rgb565"
|
||||
# Similarly, if "p8" or "p4" is specified, select the cheapest variation
|
||||
elif format_name == "p8":
|
||||
format_name = "p8_rgb565a" if has_alpha else "p8_rgb565"
|
||||
elif format_name == "p4":
|
||||
format_name = "p4_rgb565a" if has_alpha else "p4_rgb565"
|
||||
|
||||
elif name.startswith("p"):
|
||||
force_alpha = name.endswith("_rgb565a")
|
||||
if name.startswith("p8"):
|
||||
trim_palette = True
|
||||
palette_base = 0x80
|
||||
color_count = 256
|
||||
elif name.startswith("p4"):
|
||||
trim_palette = False
|
||||
palette_base = 0x00
|
||||
color_count = 16
|
||||
else:
|
||||
raise FxconvError(f"unknown palette format {profile}")
|
||||
# Otherwise, just use the format as specified
|
||||
format = CgProfile.find(format_name)
|
||||
if format is None:
|
||||
raise FxconvError(f"unknown image format '{format_name}")
|
||||
|
||||
# Encode the image into 16-bit with a palette of 16 or 256 entries
|
||||
encoded, stride, palette, alpha = r5g6b5(img, color_count=color_count,
|
||||
trim_palette=trim_palette, palette_base=palette_base,
|
||||
force_alpha=force_alpha)
|
||||
# Check that we have transparency support if we need it
|
||||
if has_alpha and not format.has_alpha:
|
||||
raise FxconvError(f"'{input}' has transparency, which {format_name} "+
|
||||
"doesn't support")
|
||||
|
||||
color_count = len(palette) // 2
|
||||
#---
|
||||
# Structure generation
|
||||
#---
|
||||
|
||||
else:
|
||||
raise FxconvError(f"unknown color profile '{name}'")
|
||||
|
||||
# Resolve "p8" and "p4" to their optimal variation
|
||||
if name == "p8":
|
||||
name = "p8_rgb565" if alpha is None else "p8_rgb565a"
|
||||
profile = CgProfile.find(name)
|
||||
elif name == "p4":
|
||||
name = "p4_rgb565" if alpha is None else "p4_rgb565a"
|
||||
profile = CgProfile.find(name)
|
||||
|
||||
if alpha is not None and not profile.supports_alpha:
|
||||
raise FxconvError(f"'{input}' has transparency; use rgb565a, " +
|
||||
"p8_rgb565a or p4_rgb565a")
|
||||
|
||||
if len(encoded) % 4 != 0:
|
||||
encoded += bytes(4 - (len(encoded) % 4))
|
||||
data, stride, palette, color_count = image_encode(img, format)
|
||||
|
||||
o = ObjectData()
|
||||
o += u8(profile.id)
|
||||
o += u8(format.id)
|
||||
o += u8(3) # DATA_RO, PALETTE_RO
|
||||
o += u16(alpha if alpha is not None else 0xffff)
|
||||
o += u16(format.alpha if format.alpha is not None else 0xffff)
|
||||
o += u16(img.width)
|
||||
o += u16(img.height)
|
||||
o += u32(stride)
|
||||
o += ref(encoded, padding=4)
|
||||
o += ref(data, padding=4)
|
||||
o += u32(color_count)
|
||||
if palette is None:
|
||||
o += u32(0)
|
||||
|
@ -856,7 +858,8 @@ def convert_libimg_cg(input, params):
|
|||
img = img.crop(area.tuple())
|
||||
|
||||
# Encode the image into 16-bit format and force the alpha to 0x0001
|
||||
encoded, stride, alpha = r5g6b5(img, force_alpha=(0x0001,0x0000))
|
||||
format = CgProfile.find("rgb565a")
|
||||
encoded, stride, palette, color_count = image_encode(img, format)
|
||||
|
||||
o = ObjectData()
|
||||
o += u16(img.width) + u16(img.height)
|
||||
|
@ -942,218 +945,141 @@ def quantize(img, dither=False):
|
|||
|
||||
return img
|
||||
|
||||
def r5g6b5(img, color_count=0, trim_palette=False, palette_base=0,
|
||||
force_alpha=None):
|
||||
def image_encode(img, format):
|
||||
"""
|
||||
Convert a PIL.Image.Image into an RGB565 byte stream. If there are
|
||||
transparent pixels, chooses a color to implement alpha and replaces them
|
||||
with this color.
|
||||
Encodes a PIL.Image.Image into one of the fx-CG image formats. The color
|
||||
depth is either RGB16, P8 or P4, with various transparency settings and
|
||||
palette encodings.
|
||||
|
||||
When color_count=0, converts the image to 16-bit; returns a bytearray of
|
||||
pixel data, the byte stride, and the automatically-chosen alpha value (or
|
||||
None).
|
||||
|
||||
* If force_alpha is a pair (value, replacement), then the alpha is forced
|
||||
to be the indicated value, and any natural occurrence of the value is
|
||||
substituted with the replacement.
|
||||
|
||||
When color_count>0, if should be either 16 or 256. The image is encoded
|
||||
with a palette of that size. Returns the converted image as a bytearray,
|
||||
the byte stride, the palette as a bytearray, and the alpha value (None if
|
||||
there were no transparent pixels).
|
||||
|
||||
* If trim_palette is set, the palette bytearray is trimmed so that only
|
||||
used entries are set (this does not include alpha, which is at the end).
|
||||
* If palette_base is provided, palette entries will be numbered starting
|
||||
from that value, wrapping around modulo color_count.
|
||||
* If force_alpha=True, an index will be reserved for alpha even if no pixel
|
||||
is transparent in the source image.
|
||||
Returns 4 values:
|
||||
* data: A bytearray containing the encoded image data
|
||||
* stride: The byte stride of the data array
|
||||
* palette: A bytearray containing the encoded palette (None if not indexed)
|
||||
* color_count: Number of colors in the palette (-1 if not indexed)
|
||||
"""
|
||||
|
||||
def rgb24to16(rgb24):
|
||||
r, g, b = rgb24
|
||||
r = (r & 0xff) >> 3
|
||||
g = (g & 0xff) >> 2
|
||||
b = (b & 0xff) >> 3
|
||||
return (r << 11) | (g << 5) | b
|
||||
|
||||
#---
|
||||
# Initial image transforms
|
||||
# Separate the alpha channel and generate a first palette.
|
||||
# Separate the alpha channel
|
||||
#---
|
||||
|
||||
# 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:
|
||||
if format.has_alpha:
|
||||
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
|
||||
else:
|
||||
alpha_channel = Image.new("1", img.size, 1)
|
||||
|
||||
if has_alpha:
|
||||
alpha_pixels = alpha_channel.load()
|
||||
|
||||
except ValueError:
|
||||
has_alpha = False
|
||||
|
||||
# Convert the input image to RGB
|
||||
alpha_pixels = alpha_channel.load()
|
||||
img = img.convert("RGB")
|
||||
|
||||
# Transparent pixels also have values on the RGB channels, so they use up a
|
||||
# palette entry (in indexed mode) or 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 }
|
||||
# Transparent pixels have random values on the RGB channels, causing them
|
||||
# to use up palette entries during quantization. To avoid that, set their
|
||||
# RGB data to a color used somewhere else in the image.
|
||||
pixels = img.load()
|
||||
bg_color = next((pixels[x,y]
|
||||
for x in range(img.width) for y in range(img.height)
|
||||
if alpha_pixels[x,y] > 0),
|
||||
(0,0,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] = bg_color
|
||||
|
||||
for y in range(img.height):
|
||||
for x in range(img.width):
|
||||
if alpha_pixels[x,y] == 0:
|
||||
pixels[x,y] = pixels[x0,y0]
|
||||
#---
|
||||
# Quantize to generate a palette
|
||||
#---
|
||||
|
||||
# In indexed mode, generate a specific palette
|
||||
if color_count:
|
||||
palette_max_size = color_count - int(has_alpha or force_alpha == True)
|
||||
if format.is_indexed:
|
||||
palette_max_size = format.color_count - int(format.has_alpha)
|
||||
img = img.convert("P",
|
||||
dither=Image.NONE,
|
||||
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
|
||||
# Note: sometimes colors are not numbered 0..N-1, so we take the max
|
||||
# value rather than len(img.getcolors()); we don't try to remap indices
|
||||
# The palette format is a list of N triplets where N includes both the
|
||||
# opaque colors we just generated and an alpha color. Sometimes colors
|
||||
# after img.convert() are not numbered 0..N-1, so take the max.
|
||||
pixels = img.load()
|
||||
N = 1 + max(pixels[x,y]
|
||||
for y in range(img.height)
|
||||
for x in range(img.width))
|
||||
palette1 = img.getpalette()[:3*N]
|
||||
palette1 = list(zip(palette1[::3], palette1[1::3], palette1[2::3]))
|
||||
for y in range(img.height) for x in range(img.width))
|
||||
|
||||
palette = img.getpalette()[:3*N]
|
||||
palette = list(zip(palette[::3], palette[1::3], palette[2::3]))
|
||||
|
||||
# For formats with transparency, make the transparent color consistent
|
||||
if format.has_alpha:
|
||||
N += 1
|
||||
palette = [(255, 0, 255)] + palette
|
||||
|
||||
# Also keep track of how to remap indices from the values generated by
|
||||
# img.convert() into the palette, which is shifted by 1 due to alpha
|
||||
# and also starts at format.palette_base. Note: format.palette_base
|
||||
# already starts 1 value later for formats with alpha.
|
||||
palette_map = [(format.palette_base + i) % format.color_count
|
||||
for i in range(N)]
|
||||
else:
|
||||
pixels = img.load()
|
||||
px = img.load()
|
||||
|
||||
#---
|
||||
# Alpha encoding
|
||||
# Find a value to encode transparency and map it into palettes.
|
||||
# Encode data into a bytearray
|
||||
#---
|
||||
|
||||
# RGB565A with fixed alpha value (fi. alpha=0x0001 in libimg)
|
||||
if color_count == 0 and force_alpha is not None:
|
||||
alpha, replacement = force_alpha
|
||||
def rgb24to16(rgb):
|
||||
r = (rgb[0] & 0xff) >> 3
|
||||
g = (rgb[1] & 0xff) >> 2
|
||||
b = (rgb[2] & 0xff) >> 3
|
||||
return (r << 11) | (g << 5) | b
|
||||
|
||||
# For palettes, anything works; use the next value
|
||||
elif color_count > 0 and (has_alpha or force_alpha == True):
|
||||
alpha = N
|
||||
|
||||
# Find an unused RGB565 value and keep the encoding to 16-bit
|
||||
elif has_alpha:
|
||||
colormap = { rgb24to16(pixels[x, y])
|
||||
for x in range(img.width) for y in range(img.height)
|
||||
if alpha_pixels[x, y] > 0 }
|
||||
|
||||
available = set(range(65536)) - colormap
|
||||
|
||||
if not available:
|
||||
raise FxconvError("image uses all 65536 colors and alpha")
|
||||
alpha = available.pop()
|
||||
|
||||
# No transparency in the image
|
||||
else:
|
||||
alpha = None
|
||||
|
||||
# Function to encode with alpha support in RGB565
|
||||
def alpha_encoding(color, a):
|
||||
if a > 0: # pixel is not transparent
|
||||
return color if color != alpha else replacement
|
||||
else:
|
||||
return alpha
|
||||
|
||||
# In palette formats, rearrange the palette to account for modulo numbering
|
||||
# and trim the palette if needed
|
||||
|
||||
if color_count > 0:
|
||||
total = len(palette1) + int(has_alpha or force_alpha == True)
|
||||
# The palette_map list associates to indices into palette1, the pixel
|
||||
# value in the data array (which starts at palette_base)
|
||||
palette_map = [(palette_base + i) % color_count for i in range(total)]
|
||||
|
||||
#---
|
||||
# Image encoding
|
||||
# Create byte arrays with pixel data and palette data
|
||||
#---
|
||||
|
||||
stride = 0
|
||||
|
||||
if not color_count:
|
||||
# In RGB16, preserve alignment between rows
|
||||
if format.depth == IMAGE_RGB16:
|
||||
# Preserve alignment between rows by padding to 4 bytes
|
||||
stride = (img.width + 1) // 2 * 4
|
||||
size = stride * img.height * 2
|
||||
elif color_count == 256:
|
||||
# No constraint in P8
|
||||
elif format.depth == IMAGE_P8:
|
||||
size = img.width * img.height
|
||||
stride = img.width
|
||||
elif color_count == 16:
|
||||
# In P4, pad whole bytes
|
||||
elif format.depth == IMAGE_P4:
|
||||
# Pad whole bytes
|
||||
stride = (img.width + 1) // 2
|
||||
size = stride * img.height
|
||||
|
||||
# Result of encoding
|
||||
encoded = bytearray(size)
|
||||
# Offset into the array
|
||||
offset = 0
|
||||
# Encode the pixel data
|
||||
data = bytearray(size)
|
||||
|
||||
for y in range(img.height):
|
||||
for x in range(img.width):
|
||||
a = alpha_pixels[x, y] if has_alpha else 255
|
||||
a = alpha_pixels[x,y]
|
||||
|
||||
if not color_count:
|
||||
c = alpha_encoding(rgb24to16(pixels[x, y]), a)
|
||||
if format.depth == IMAGE_RGB16:
|
||||
# If c lands on the alpha value, flip its lowest bit
|
||||
c = rgb24to16(pixels[x, y])
|
||||
c = format.alpha if (a == 0) else c ^ (c == format.alpha)
|
||||
offset = (stride * y) + x * 2
|
||||
encoded[offset:offset+2] = u16(c)
|
||||
data[offset:offset+2] = u16(c)
|
||||
|
||||
elif color_count == 16:
|
||||
c = palette_map[pixels[x, y] if a > 0 else alpha]
|
||||
elif format.depth == IMAGE_P8:
|
||||
c = palette_map[pixels[x, y]] if a > 0 else format.alpha
|
||||
offset = (stride * y) + x
|
||||
data[offset] = c
|
||||
|
||||
# Select either the 4 MSb or 4 LSb of the current byte
|
||||
elif format.depth == IMAGE_P4:
|
||||
c = palette_map[pixels[x, y]] if a > 0 else format.alpha
|
||||
offset = (stride * y) + (x // 2)
|
||||
if x % 2 == 0:
|
||||
encoded[offset] |= (c << 4)
|
||||
data[offset] |= (c << 4)
|
||||
else:
|
||||
encoded[offset] |= c
|
||||
data[offset] |= c
|
||||
|
||||
offset += (x % 2 == 1) or (x == img.width - 1)
|
||||
# Encode the palette
|
||||
|
||||
elif color_count == 256:
|
||||
c = palette_map[pixels[x, y] if a > 0 else alpha]
|
||||
encoded[offset] = c
|
||||
offset += 1
|
||||
|
||||
# Encode the palette as R5G6B5
|
||||
|
||||
if color_count > 0:
|
||||
if trim_palette:
|
||||
encoded_palette = bytearray(2 * len(palette_map))
|
||||
else:
|
||||
encoded_palette = bytearray(2 * color_count)
|
||||
|
||||
for i in range(len(palette1)):
|
||||
encoded_palette[2*i:2*i+2] = u16(rgb24to16(palette1[i]))
|
||||
|
||||
#---
|
||||
# Outro
|
||||
#---
|
||||
|
||||
if color_count > 0:
|
||||
if alpha is not None:
|
||||
alpha = palette_map[alpha]
|
||||
return encoded, stride, encoded_palette, alpha
|
||||
if format.is_indexed:
|
||||
N = N if format.trim_palette else format.color_count
|
||||
encoded_palette = bytearray(2 * N)
|
||||
for i, rgb24 in enumerate(palette):
|
||||
encoded_palette[2*i:2*i+2] = u16(rgb24to16(rgb24))
|
||||
return data, stride, encoded_palette, N
|
||||
else:
|
||||
return encoded, stride, alpha
|
||||
return data, stride, None, -1
|
||||
|
||||
def convert(input, params, target, output=None, model=None, custom=None):
|
||||
"""
|
||||
|
|
Loading…
Reference in a new issue