fxconv: add support for {P4,P8}_{RGB565,RGB565A} for Azur

pull/9/head
Lephenixnoir 11 months ago
parent 68be7fe522
commit 317b82348f
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
  1. 240
      fxconv/fxconv.py

@ -108,13 +108,22 @@ class CgProfile:
return None
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),
# 16-bit R5G6B5 with alpha
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),
# 4-bit palette
# The original 4-bit palette mode of bopti (same as P4_RGB565A)
CgProfile(0x3, "p4", True),
]
@ -515,40 +524,51 @@ 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 r5g6b5 or r5g6b5a later on
# 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)
if name in [ "r5g6b5", "r5g6b5a", None ]:
if name in ["r5g6b5", "r5g6b5a", "rgb565", "rgb565a", None]:
# Encode the image into the 16-bit format
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)
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
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
else:
raise FxconvError(f"unknown color profile '{name}'")
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([
0x00, profile.id, # Profile identification
a >> 8, a & 0xff, # Alpha color
w >> 8, w & 0xff, # Width
h >> 8, h & 0xff, # Height
])
if name in ["p8_rgb565", "p8_rgb565a"]:
header += u16(color_count)
if len(encoded) % 4 != 0:
encoded += bytes(4 - (len(encoded) % 4))
return header + encoded
#
@ -901,9 +921,9 @@ def quantize(img, dither=False):
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
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
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).
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.
"""
def rgb24to16(r, g, b):
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
# 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:
alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE)
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
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:
palette_size = color_count - int(has_alpha)
img = img.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE,
colors=palette_size)
palette = img.getpalette()
palette_max_size = color_count - int(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
palette1 = img.getpalette()[:3*len(img.getcolors())]
palette1 = list(zip(palette1[::3], palette1[1::3], palette1[2::3]))
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:
alpha, replacement = alpha
if color_count > 0:
raise FxconvError("cannot choose alpha value in palette formats")
else:
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
# Alpha always uses color index 0 in palettes (helps faster rendering)
elif color_count > 0 and has_alpha:
alpha = 0
# Find an unused RGB565 value and keep the encoding to 16-bit
elif has_alpha:
# Compute the set of all used R5G6B5 colors
colormap = set()
colormap = { rgb24to16(pixels[x, y])
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
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:
if color == alpha:
return replacement
else:
return color
if a > 0: # pixel is not transparent
return color if color != alpha else replacement
else:
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
@ -998,58 +1094,58 @@ def r5g6b5(img, color_count=0, alpha=None):
elif color_count == 256:
size = pixel_count
elif color_count == 16:
size = (pixel_count + 1) // 2
size = ((img.width + 1) // 2) * img.height
# Result of encoding
encoded = bytearray(size)
# Number of pixels encoded so far
entries = 0
# Offset into the array
offset = 0
for y in range(img.height):
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:
c = alpha_encoding(rgb24to16(*pixels[x, y]), a)
encoded[offset] = c >> 8
encoded[offset+1] = c & 0xff
c = alpha_encoding(rgb24to16(pixels[x, y]), a)
encoded[offset:offset+2] = u16(c)
offset += 2
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
if (entries % 2) == 0:
# Select either the 4 MSb or 4 LSb of the current byte
if x % 2 == 0:
encoded[offset] |= (c << 4)
# Unaligned pixels: right 4 bits of current byte
else:
encoded[offset] |= c
offset += 1
offset += (x % 2 == 1) or (x == img.width - 1)
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
offset += 1
entries += 1
if not color_count:
return encoded, alpha
# 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)):
r, g, b = palette[3*c], palette[3*c+1], palette[3*c+2]
rgb16 = rgb24to16(r, g, b)
for i in range(len(palette1)):
index = palette_remap[i][1]
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):
"""

Loading…
Cancel
Save