From 0d37a6c848a8e5d7cc7c38b7d41b52542c91518b Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Mon, 26 May 2025 16:20:13 +0800 Subject: [PATCH 01/12] fix: Texture2DConverter - Alpha8 swizzle --- UnityPy/export/Texture2DConverter.py | 45 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 3068a6a2..2478ead9 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -1,4 +1,4 @@ -from __future__ import annotations +from __future__ import annotations import struct from copy import copy @@ -193,11 +193,11 @@ def image_to_texture2d( elif target_texture_format in [TF.ETC2_RGBA8, TF.ETC2_RGBA8Crunched, TF.ETC2_RGBA1]: tex_format = TF.ETC2_RGBA8 compress_func = compress_etcpak - # A + # L elif target_texture_format == TF.Alpha8: tex_format = TF.Alpha8 - pil_mode = "A" - # R - should probably be moerged into #A, as pure R is used as Alpha + pil_mode = "L" + # R - should probably be merged into #L, as pure R is used as Alpha # but need test data for this first elif target_texture_format in [ TF.R8, @@ -229,18 +229,20 @@ def image_to_texture2d( if tex_format == TextureFormat.RGB24: s_tex_format = TextureFormat.RGBA32 pil_mode = "RGBA" - # elif tex_format == TextureFormat.BGR24: - # s_tex_format = TextureFormat.BGRA32 + elif tex_format == TextureFormat.BGR24: + s_tex_format = TextureFormat.BGRA32 + pil_mode = "BGRA" + block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[s_tex_format] width, height = TextureSwizzler.get_padded_texture_size( img.width, img.height, *block_size, gobsPerBlock ) img = pad_image(img, width, height) img = Image.frombytes( - "RGBA", + pil_mode, img.size, TextureSwizzler.swizzle( - img.tobytes("raw", "RGBA"), width, height, *block_size, gobsPerBlock + img.tobytes("raw", pil_mode), width, height, *block_size, gobsPerBlock ), ) @@ -317,15 +319,23 @@ def parse_image_data( switch_swizzle = None if platform == BuildTarget.Switch and platform_blob is not None: gobsPerBlock = TextureSwizzler.get_switch_gobs_per_block(platform_blob) + s_tex_format = texture_format + pil_mode = "RGBA" + if texture_format == TextureFormat.RGB24: - texture_format = TextureFormat.RGBA32 + s_tex_format = TextureFormat.RGBA32 elif texture_format == TextureFormat.BGR24: - texture_format = TextureFormat.BGRA32 - block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[texture_format] + s_tex_format = TextureFormat.BGRA32 + pil_mode = "BGRA" + elif texture_format == TextureFormat.Alpha8: + s_tex_format = texture_format + pil_mode = "L" + + block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[s_tex_format] width, height = TextureSwizzler.get_padded_texture_size( width, height, *block_size, gobsPerBlock ) - switch_swizzle = (block_size, gobsPerBlock) + switch_swizzle = (block_size, gobsPerBlock, pil_mode) else: width, height = get_compressed_image_size(width, height, texture_format) @@ -349,10 +359,13 @@ def parse_image_data( img = selection[0](image_data, width, height, *selection[1:]) if switch_swizzle is not None: - image_data = TextureSwizzler.deswizzle( - img.tobytes("raw", "RGBA"), width, height, *block_size, gobsPerBlock + block_size, gobsPerBlock, pil_mode = switch_swizzle + swizzle_data = bytes( + TextureSwizzler.deswizzle( + img.tobytes("raw", pil_mode), width, height, *block_size, gobsPerBlock + ) ) - img = Image.frombytes(img.mode, (width, height), image_data, "raw", "RGBA") + img = Image.frombytes(img.mode, (width, height), swizzle_data, "raw", pil_mode) if original_width != width or original_height != height: img = img.crop((0, 0, original_width, original_height)) @@ -540,7 +553,7 @@ def rgb9e5float(image_data: bytes, width: int, height: int) -> Image.Image: CONV_TABLE = { # FORMAT FUNC #ARGS..... # ----------------------- -------- -------- ------------ ----------------- ------------ ---------- - (TF.Alpha8, pillow, "RGBA", "raw", "A"), + (TF.Alpha8, pillow, "L", "raw", "L"), (TF.ARGB4444, pillow, "RGBA", "raw", "RGBA;4B", (2, 1, 0, 3)), (TF.RGB24, pillow, "RGB", "raw", "RGB"), (TF.RGBA32, pillow, "RGBA", "raw", "RGBA"), From b8a8f8be9d006f348abe8af13ab60c8fc9eb2e60 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Mon, 26 May 2025 16:41:11 +0800 Subject: [PATCH 02/12] fix: Texture2DConverter - bytes is not mutable --- UnityPy/export/Texture2DConverter.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 2478ead9..e60da415 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -377,12 +377,11 @@ def parse_image_data( def swap_bytes_for_xbox(image_data: bytes) -> bytes: - """swaps the texture bytes - This is required for textures deployed on XBOX360. - """ - for i in range(0, len(image_data), 2): - image_data[i : i + 2] = image_data[i : i + 2][::-1] - return image_data + """Swaps the texture bytes for XBOX360.""" + data = bytearray(image_data) + for i in range(0, len(data), 2): + data[i : i + 2] = data[i : i + 2][::-1] + return bytes(data) def pillow( From 76bddcdbfa407e136592f86c4cc827850e3ab235 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Mon, 26 May 2025 16:44:56 +0800 Subject: [PATCH 03/12] fix: Texture2DConverter - clean up code --- UnityPy/export/Texture2DConverter.py | 49 ++++++++++++---------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index e60da415..0ebe604c 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -1,7 +1,6 @@ from __future__ import annotations import struct -from copy import copy from io import BytesIO from threading import Lock from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union @@ -122,9 +121,9 @@ def compress_astc( astc_encoder.ASTCType.U8, width, height, 1, data ) block_size = TEXTURE_FORMAT_BLOCK_SIZE_TABLE[target_texture_format] - assert block_size is not None, ( - f"failed to get block size for {target_texture_format.name}" - ) + assert ( + block_size is not None + ), f"failed to get block size for {target_texture_format.name}" swizzle = astc_encoder.ASTCSwizzle.from_str("RGBA") context, lock = get_astc_context(block_size) @@ -152,21 +151,14 @@ def image_to_texture2d( tex_format = TF.RGBA32 pil_mode = "RGBA" - # DXT + # DXT / BC if target_texture_format in [TF.DXT1, TF.DXT1Crunched]: tex_format = TF.DXT1 compress_func = compress_etcpak elif target_texture_format in [TF.DXT5, TF.DXT5Crunched]: tex_format = TF.DXT5 compress_func = compress_etcpak - elif target_texture_format in [TF.BC4]: - tex_format = TF.BC4 - compress_func = compress_etcpak - elif target_texture_format in [TF.BC5]: - tex_format = TF.BC5 - compress_func = compress_etcpak - elif target_texture_format in [TF.BC7]: - tex_format = TF.BC7 + elif target_texture_format in [TF.BC4, TF.BC5, TF.BC7]: compress_func = compress_etcpak # ASTC elif target_texture_format.name.startswith("ASTC"): @@ -224,7 +216,7 @@ def image_to_texture2d( # everything else defaulted to RGBA if platform == BuildTarget.Switch and platform_blob is not None: - gobsPerBlock = TextureSwizzler.get_switch_gobs_per_block(platform_blob) + gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) s_tex_format = tex_format if tex_format == TextureFormat.RGB24: s_tex_format = TextureFormat.RGBA32 @@ -235,14 +227,14 @@ def image_to_texture2d( block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[s_tex_format] width, height = TextureSwizzler.get_padded_texture_size( - img.width, img.height, *block_size, gobsPerBlock + img.width, img.height, *block_size, gobs_per_block ) img = pad_image(img, width, height) img = Image.frombytes( pil_mode, img.size, TextureSwizzler.swizzle( - img.tobytes("raw", pil_mode), width, height, *block_size, gobsPerBlock + img.tobytes("raw", pil_mode), width, height, *block_size, gobs_per_block ), ) @@ -261,9 +253,9 @@ def image_to_texture2d( def assert_rgba(img: Image.Image, target_texture_format: TextureFormat) -> Image.Image: if img.mode == "RGB": img = img.convert("RGBA") - assert img.mode == "RGBA", ( - f"{target_texture_format} compression only supports RGB & RGBA images" - ) # noqa: E501 + assert ( + img.mode == "RGBA" + ), f"{target_texture_format} compression only supports RGB & RGBA images" # noqa: E501 return img @@ -305,7 +297,7 @@ def parse_image_data( if not width or not height: return Image.new("RGBA", (0, 0)) - image_data = copy(bytes(image_data)) + image_data = bytes(image_data) if not image_data: raise ValueError("Texture2D has no image data") @@ -315,10 +307,10 @@ def parse_image_data( if platform == BuildTarget.XBOX360 and texture_format in XBOX_SWAP_FORMATS: image_data = swap_bytes_for_xbox(image_data) - original_width, original_height = (width, height) + original_width, original_height = width, height switch_swizzle = None if platform == BuildTarget.Switch and platform_blob is not None: - gobsPerBlock = TextureSwizzler.get_switch_gobs_per_block(platform_blob) + gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) s_tex_format = texture_format pil_mode = "RGBA" @@ -333,19 +325,18 @@ def parse_image_data( block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[s_tex_format] width, height = TextureSwizzler.get_padded_texture_size( - width, height, *block_size, gobsPerBlock + width, height, *block_size, gobs_per_block ) - switch_swizzle = (block_size, gobsPerBlock, pil_mode) + switch_swizzle = (block_size, gobs_per_block, pil_mode) else: width, height = get_compressed_image_size(width, height, texture_format) selection = CONV_TABLE[texture_format] - if len(selection) == 0: + if not selection: raise NotImplementedError(f"Not implemented texture format: {texture_format}") if "Crunched" in texture_format.name: - version = version if ( version[0] > 2017 or (version[0] == 2017 and version[1] >= 3) # 2017.3 and up @@ -359,10 +350,10 @@ def parse_image_data( img = selection[0](image_data, width, height, *selection[1:]) if switch_swizzle is not None: - block_size, gobsPerBlock, pil_mode = switch_swizzle + block_size, gobs_per_block, pil_mode = switch_swizzle swizzle_data = bytes( TextureSwizzler.deswizzle( - img.tobytes("raw", pil_mode), width, height, *block_size, gobsPerBlock + img.tobytes("raw", pil_mode), width, height, *block_size, gobs_per_block ) ) img = Image.frombytes(img.mode, (width, height), swizzle_data, "raw", pil_mode) @@ -477,7 +468,7 @@ def etc(image_data: bytes, width: int, height: int, fmt: list) -> Image.Image: return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") -def eac(image_data: bytes, width: int, height: int, fmt: list) -> Image.Image: +def eac(image_data: bytes, width: int, height: int, fmt: str) -> Image.Image: if fmt == "EAC_R": image_data = texture2ddecoder.decode_eacr(image_data, width, height) elif fmt == "EAC_R_SIGNED": From 58139c2beb46cca1ab8dffc62d5e7d3120e11b0f Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Mon, 26 May 2025 17:34:51 +0800 Subject: [PATCH 04/12] refactor: Texture2DConverter - optimize `CONV_TABLE` --- UnityPy/export/Texture2DConverter.py | 197 +++++++++++++-------------- 1 file changed, 97 insertions(+), 100 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 0ebe604c..81320061 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -3,7 +3,7 @@ import struct from io import BytesIO from threading import Lock -from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Callable, Dict, Optional, Tuple, Union import astc_encoder import texture2ddecoder @@ -331,11 +331,6 @@ def parse_image_data( else: width, height = get_compressed_image_size(width, height, texture_format) - selection = CONV_TABLE[texture_format] - - if not selection: - raise NotImplementedError(f"Not implemented texture format: {texture_format}") - if "Crunched" in texture_format.name: if ( version[0] > 2017 @@ -347,7 +342,11 @@ def parse_image_data( else: image_data = texture2ddecoder.unpack_crunch(image_data) - img = selection[0](image_data, width, height, *selection[1:]) + conv_func = CONV_TABLE.get(texture_format) + if not conv_func: + raise NotImplementedError(f"Not implemented texture format: {texture_format}") + + img = conv_func(image_data, width, height) if switch_swizzle is not None: block_size, gobs_per_block, pil_mode = switch_swizzle @@ -453,18 +452,18 @@ def pvrtc(image_data: bytes, width: int, height: int, fmt: bool) -> Image.Image: return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") -def etc(image_data: bytes, width: int, height: int, fmt: list) -> Image.Image: - if fmt[0] == 1: +def etc(image_data: bytes, width: int, height: int, fmt: str) -> Image.Image: + if fmt == "ETC1": image_data = texture2ddecoder.decode_etc1(image_data, width, height) - elif fmt[0] == 2: - if fmt[1] == "RGB": - image_data = texture2ddecoder.decode_etc2(image_data, width, height) - elif fmt[1] == "A1": - image_data = texture2ddecoder.decode_etc2a1(image_data, width, height) - elif fmt[1] == "A8": - image_data = texture2ddecoder.decode_etc2a8(image_data, width, height) + elif fmt == "ETC2_RGB": + image_data = texture2ddecoder.decode_etc2(image_data, width, height) + elif fmt == "ETC2_A1": + image_data = texture2ddecoder.decode_etc2a1(image_data, width, height) + elif fmt == "ETC2_A8": + image_data = texture2ddecoder.decode_etc2a8(image_data, width, height) else: - raise NotImplementedError("unknown etc mode") + raise NotImplementedError(f"Unknown ETC mode: {fmt}") + return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") @@ -477,6 +476,9 @@ def eac(image_data: bytes, width: int, height: int, fmt: str) -> Image.Image: image_data = texture2ddecoder.decode_eacrg(image_data, width, height) elif fmt == "EAC_RG_SIGNED": image_data = texture2ddecoder.decode_eacrg_signed(image_data, width, height) + else: + raise NotImplementedError(f"Unknown EAC mode: {fmt}") + return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") @@ -540,90 +542,85 @@ def rgb9e5float(image_data: bytes, width: int, height: int) -> Image.Image: return Image.frombytes("RGB", (width, height), rgb, "raw", "RGB") -CONV_TABLE = { - # FORMAT FUNC #ARGS..... - # ----------------------- -------- -------- ------------ ----------------- ------------ ---------- - (TF.Alpha8, pillow, "L", "raw", "L"), - (TF.ARGB4444, pillow, "RGBA", "raw", "RGBA;4B", (2, 1, 0, 3)), - (TF.RGB24, pillow, "RGB", "raw", "RGB"), - (TF.RGBA32, pillow, "RGBA", "raw", "RGBA"), - (TF.ARGB32, pillow, "RGBA", "raw", "ARGB"), - (TF.ARGBFloat, pillow, "RGBA", "raw", "RGBAF", (2, 1, 0, 3)), - (TF.RGB565, pillow, "RGB", "raw", "BGR;16"), - (TF.BGR24, pillow, "RGB", "raw", "BGR"), - (TF.R8, pillow, "RGB", "raw", "R"), - (TF.R16, pillow, "RGB", "raw", "R;16"), - (TF.RG16, rg, "RGB", "raw", "RG"), - (TF.DXT1, pillow, "RGBA", "bcn", 1), - (TF.DXT3, pillow, "RGBA", "bcn", 2), - (TF.DXT5, pillow, "RGBA", "bcn", 3), - (TF.RGBA4444, pillow, "RGBA", "raw", "RGBA;4B", (3, 2, 1, 0)), - (TF.BGRA32, pillow, "RGBA", "raw", "BGRA"), - (TF.RHalf, half, "R", "raw", "R"), - (TF.RGHalf, rg, "RGB", "raw", "RGE"), - (TF.RGBAHalf, half, "RGB", "raw", "RGB"), - (TF.RFloat, pillow, "RGB", "raw", "RF"), - (TF.RGFloat, rg, "RGB", "raw", "RGF"), - (TF.RGBAFloat, pillow, "RGBA", "raw", "RGBAF"), - (TF.YUY2,), - (TF.RGB9e5Float, rgb9e5float), - (TF.BC4, pillow, "L", "bcn", 4), - (TF.BC5, pillow, "RGB", "bcn", 5), - (TF.BC6H, pillow, "RGBA", "bcn", 6), - (TF.BC7, pillow, "RGBA", "bcn", 7), - (TF.DXT1Crunched, pillow, "RGBA", "bcn", 1), - (TF.DXT5Crunched, pillow, "RGBA", "bcn", 3), - (TF.PVRTC_RGB2, pvrtc, True), - (TF.PVRTC_RGBA2, pvrtc, True), - (TF.PVRTC_RGB4, pvrtc, False), - (TF.PVRTC_RGBA4, pvrtc, False), - (TF.ETC_RGB4, etc, (1,)), - (TF.ATC_RGB4, atc, False), - (TF.ATC_RGBA8, atc, True), - (TF.EAC_R, eac, "EAC_R"), - (TF.EAC_R_SIGNED, eac, "EAC_R:SIGNED"), - (TF.EAC_RG, eac, "EAC_RG"), - (TF.EAC_RG_SIGNED, eac, "EAC_RG_SIGNED"), - (TF.ETC2_RGB, etc, (2, "RGB")), - (TF.ETC2_RGBA1, etc, (2, "A1")), - (TF.ETC2_RGBA8, etc, (2, "A8")), - (TF.ASTC_RGB_4x4, astc, (4, 4)), - (TF.ASTC_RGB_5x5, astc, (5, 5)), - (TF.ASTC_RGB_6x6, astc, (6, 6)), - (TF.ASTC_RGB_8x8, astc, (8, 8)), - (TF.ASTC_RGB_10x10, astc, (10, 10)), - (TF.ASTC_RGB_12x12, astc, (12, 12)), - (TF.ASTC_RGBA_4x4, astc, (4, 4)), - (TF.ASTC_RGBA_5x5, astc, (5, 5)), - (TF.ASTC_RGBA_6x6, astc, (6, 6)), - (TF.ASTC_RGBA_8x8, astc, (8, 8)), - (TF.ASTC_RGBA_10x10, astc, (10, 10)), - (TF.ASTC_RGBA_12x12, astc, (12, 12)), - (TF.ETC_RGB4_3DS, etc, (1,)), - (TF.ETC_RGBA8_3DS, etc, (1,)), - (TF.ETC_RGB4Crunched, etc, (1,)), - (TF.ETC2_RGBA8Crunched, etc, (2, "A8")), - (TF.ASTC_HDR_4x4, astc, (4, 4)), - (TF.ASTC_HDR_5x5, astc, (5, 5)), - (TF.ASTC_HDR_6x6, astc, (6, 6)), - (TF.ASTC_HDR_8x8, astc, (8, 8)), - (TF.ASTC_HDR_10x10, astc, (10, 10)), - (TF.ASTC_HDR_12x12, astc, (12, 12)), - (TF.RG32, rg, "RGB", "raw", "RG;16"), - (TF.RGB48, pillow, "RGB", "raw", "RGB;16"), - (TF.RGBA64, pillow, "RGBA", "raw", "RGBA;16"), - (TF.R8_SIGNED, pillow, "R", "raw", "R;8s"), - (TF.RG16_SIGNED, rg, "RGB", "raw", "RG;8s"), - (TF.RGB24_SIGNED, pillow, "RGB", "raw", "RGB;8s"), - (TF.RGBA32_SIGNED, pillow, "RGBA", "raw", "RGBA;8s"), - (TF.R16_SIGNED, pillow, "R", "raw", "R;16s"), - (TF.RG32_SIGNED, rg, "RGB", "raw", "RG;16s"), - (TF.RGB48_SIGNED, pillow, "RGB", "raw", "RGB;16s"), - (TF.RGBA64_SIGNED, pillow, "RGBA", "raw", "RGBA;16s"), +CONV_TABLE: Dict[TextureFormat, Callable[[bytes, int, int], Image.Image]] = { + TF.Alpha8: lambda data, w, h: pillow(data, w, h, "L", "raw", "L"), + TF.ARGB4444: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;4B", (2, 1, 0, 3)), + TF.RGB24: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RGB"), + TF.RGBA32: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA"), + TF.ARGB32: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "ARGB"), + TF.ARGBFloat: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBAF", (2, 1, 0, 3)), + TF.RGB565: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "BGR;16"), + TF.BGR24: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "BGR"), + TF.R8: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "R"), + TF.R16: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "R;16"), + TF.RG16: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RG"), + TF.DXT1: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 1), + TF.DXT3: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 2), + TF.DXT5: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 3), + TF.RGBA4444: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;4B", (3, 2, 1, 0)), + TF.BGRA32: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "BGRA"), + TF.RHalf: lambda data, w, h: half(data, w, h, "R", "raw", "R"), + TF.RGHalf: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RGE"), + TF.RGBAHalf: lambda data, w, h: half(data, w, h, "RGB", "raw", "RGB"), + TF.RFloat: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RF"), + TF.RGFloat: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RGF"), + TF.RGBAFloat: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBAF"), + # TF.YUY2: lambda data, w, h: NotImplementedError("YUY2 not implemented"), + TF.RGB9e5Float: lambda data, w, h: rgb9e5float(data, w, h), + TF.BC4: lambda data, w, h: pillow(data, w, h, "L", "bcn", 4), + TF.BC5: lambda data, w, h: pillow(data, w, h, "RGB", "bcn", 5), + TF.BC6H: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 6), + TF.BC7: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 7), + TF.DXT1Crunched: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 1), + TF.DXT5Crunched: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 3), + TF.PVRTC_RGB2: lambda data, w, h: pvrtc(data, w, h, True), + TF.PVRTC_RGBA2: lambda data, w, h: pvrtc(data, w, h, True), + TF.PVRTC_RGB4: lambda data, w, h: pvrtc(data, w, h, False), + TF.PVRTC_RGBA4: lambda data, w, h: pvrtc(data, w, h, False), + TF.ETC_RGB4: lambda data, w, h: etc(data, w, h, "ETC1"), + TF.ATC_RGB4: lambda data, w, h: atc(data, w, h, False), + TF.ATC_RGBA8: lambda data, w, h: atc(data, w, h, True), + TF.EAC_R: lambda data, w, h: eac(data, w, h, "EAC_R"), + TF.EAC_R_SIGNED: lambda data, w, h: eac(data, w, h, "EAC_R_SIGNED"), + TF.EAC_RG: lambda data, w, h: eac(data, w, h, "EAC_RG"), + TF.EAC_RG_SIGNED: lambda data, w, h: eac(data, w, h, "EAC_RG_SIGNED"), + TF.ETC2_RGB: lambda data, w, h: etc(data, w, h, "ETC2_RGB"), + TF.ETC2_RGBA1: lambda data, w, h: etc(data, w, h, "ETC2_A1"), + TF.ETC2_RGBA8: lambda data, w, h: etc(data, w, h, "ETC2_A8"), + TF.ASTC_RGB_4x4: lambda data, w, h: astc(data, w, h, (4, 4)), + TF.ASTC_RGB_5x5: lambda data, w, h: astc(data, w, h, (5, 5)), + TF.ASTC_RGB_6x6: lambda data, w, h: astc(data, w, h, (6, 6)), + TF.ASTC_RGB_8x8: lambda data, w, h: astc(data, w, h, (8, 8)), + TF.ASTC_RGB_10x10: lambda data, w, h: astc(data, w, h, (10, 10)), + TF.ASTC_RGB_12x12: lambda data, w, h: astc(data, w, h, (12, 12)), + TF.ASTC_RGBA_4x4: lambda data, w, h: astc(data, w, h, (4, 4)), + TF.ASTC_RGBA_5x5: lambda data, w, h: astc(data, w, h, (5, 5)), + TF.ASTC_RGBA_6x6: lambda data, w, h: astc(data, w, h, (6, 6)), + TF.ASTC_RGBA_8x8: lambda data, w, h: astc(data, w, h, (8, 8)), + TF.ASTC_RGBA_10x10: lambda data, w, h: astc(data, w, h, (10, 10)), + TF.ASTC_RGBA_12x12: lambda data, w, h: astc(data, w, h, (12, 12)), + TF.ETC_RGB4_3DS: lambda data, w, h: etc(data, w, h, "ETC1"), + TF.ETC_RGBA8_3DS: lambda data, w, h: etc(data, w, h, "ETC1"), + TF.ETC_RGB4Crunched: lambda data, w, h: etc(data, w, h, "ETC1"), + TF.ETC2_RGBA8Crunched: lambda data, w, h: etc(data, w, h, "ETC2_A8"), + TF.ASTC_HDR_4x4: lambda data, w, h: astc(data, w, h, (4, 4)), + TF.ASTC_HDR_5x5: lambda data, w, h: astc(data, w, h, (5, 5)), + TF.ASTC_HDR_6x6: lambda data, w, h: astc(data, w, h, (6, 6)), + TF.ASTC_HDR_8x8: lambda data, w, h: astc(data, w, h, (8, 8)), + TF.ASTC_HDR_10x10: lambda data, w, h: astc(data, w, h, (10, 10)), + TF.ASTC_HDR_12x12: lambda data, w, h: astc(data, w, h, (12, 12)), + TF.RG32: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RG;16"), + TF.RGB48: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RGB;16"), + TF.RGBA64: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;16"), + TF.R8_SIGNED: lambda data, w, h: pillow(data, w, h, "R", "raw", "R;8s"), + TF.RG16_SIGNED: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RG;8s"), + TF.RGB24_SIGNED: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RGB;8s"), + TF.RGBA32_SIGNED: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;8s"), + TF.R16_SIGNED: lambda data, w, h: pillow(data, w, h, "R", "raw", "R;16s"), + TF.RG32_SIGNED: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RG;16s"), + TF.RGB48_SIGNED: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RGB;16s"), + TF.RGBA64_SIGNED: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;16s"), } -# format conv_table to a dict -CONV_TABLE = {line[0]: line[1:] for line in CONV_TABLE} - # XBOX Swap Formats XBOX_SWAP_FORMATS = [TF.RGB565, TF.DXT1, TF.DXT1Crunched, TF.DXT5, TF.DXT5Crunched] From 567a5678401b5265dd357bf132ebdd28b3b822e4 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Mon, 26 May 2025 20:49:15 +0800 Subject: [PATCH 05/12] fix: Texture2DConverter - undue swizzle --- UnityPy/export/Texture2DConverter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 81320061..5b0b7b32 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -215,7 +215,7 @@ def image_to_texture2d( pil_mode = "RGB" # everything else defaulted to RGBA - if platform == BuildTarget.Switch and platform_blob is not None: + if platform == BuildTarget.Switch and platform_blob: gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) s_tex_format = tex_format if tex_format == TextureFormat.RGB24: @@ -309,7 +309,7 @@ def parse_image_data( original_width, original_height = width, height switch_swizzle = None - if platform == BuildTarget.Switch and platform_blob is not None: + if platform == BuildTarget.Switch and platform_blob: gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) s_tex_format = texture_format pil_mode = "RGBA" @@ -355,6 +355,7 @@ def parse_image_data( img.tobytes("raw", pil_mode), width, height, *block_size, gobs_per_block ) ) + img.mode = pil_mode img = Image.frombytes(img.mode, (width, height), swizzle_data, "raw", pil_mode) if original_width != width or original_height != height: From e1b6f0b431bbe6b17187f2d551fb0fe51e328612 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Mon, 26 May 2025 22:20:21 +0800 Subject: [PATCH 06/12] fix: Texture2DConverter - RGB24 and BGR24 swizzle --- UnityPy/export/Texture2DConverter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 5b0b7b32..6159573c 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -217,6 +217,7 @@ def image_to_texture2d( if platform == BuildTarget.Switch and platform_blob: gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) + s_tex_format = tex_format if tex_format == TextureFormat.RGB24: s_tex_format = TextureFormat.RGBA32 @@ -311,19 +312,18 @@ def parse_image_data( switch_swizzle = None if platform == BuildTarget.Switch and platform_blob: gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) - s_tex_format = texture_format - pil_mode = "RGBA" + pil_mode = "RGBA" if texture_format == TextureFormat.RGB24: - s_tex_format = TextureFormat.RGBA32 + texture_format = TextureFormat.RGBA32 elif texture_format == TextureFormat.BGR24: - s_tex_format = TextureFormat.BGRA32 + texture_format = TextureFormat.BGRA32 pil_mode = "BGRA" elif texture_format == TextureFormat.Alpha8: - s_tex_format = texture_format + texture_format = texture_format pil_mode = "L" - block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[s_tex_format] + block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[texture_format] width, height = TextureSwizzler.get_padded_texture_size( width, height, *block_size, gobs_per_block ) From e144bcd87adcb3abe3bc81a2043dc8c0bafb5a62 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Tue, 27 May 2025 00:33:02 +0800 Subject: [PATCH 07/12] chore: Texture2D - add assertions for typecheck --- UnityPy/classes/legacy_patch/Texture2D.py | 2 ++ UnityPy/classes/legacy_patch/Texture2DArray.py | 2 ++ UnityPy/export/Texture2DConverter.py | 1 + 3 files changed, 5 insertions(+) diff --git a/UnityPy/classes/legacy_patch/Texture2D.py b/UnityPy/classes/legacy_patch/Texture2D.py index 9b4b2af3..7d9d0ab5 100644 --- a/UnityPy/classes/legacy_patch/Texture2D.py +++ b/UnityPy/classes/legacy_patch/Texture2D.py @@ -24,6 +24,7 @@ def _Texture2d_set_image( if not isinstance(img, Image.Image): img = Image.open(img) + assert isinstance(img, Image.Image) platform = self.object_reader.platform if self.object_reader is not None else 0 img_data, tex_format = Texture2DConverter.image_to_texture2d( @@ -70,6 +71,7 @@ def _Texture2D_get_image_data(self: Texture2D): if self.m_StreamData: from ...helpers.ResourceReader import get_resource_data + assert self.object_reader is not None return get_resource_data( self.m_StreamData.path, self.object_reader.assets_file, diff --git a/UnityPy/classes/legacy_patch/Texture2DArray.py b/UnityPy/classes/legacy_patch/Texture2DArray.py index e5591bca..c708d8d5 100644 --- a/UnityPy/classes/legacy_patch/Texture2DArray.py +++ b/UnityPy/classes/legacy_patch/Texture2DArray.py @@ -15,9 +15,11 @@ def _Texture2DArray_get_images(self: Texture2DArray) -> List[Image.Image]: texture_format = GRAPHICS_TO_TEXTURE_MAP.get(GraphicsFormat(self.m_Format)) if not texture_format: raise NotImplementedError(f"GraphicsFormat {self.m_Format} not supported yet") + assert self.object_reader is not None image_data = self.image_data if image_data is None: + assert self.m_StreamData is not None image_data = get_resource_data( self.m_StreamData.path, self.object_reader.assets_file, diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 6159573c..06fdec08 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -273,6 +273,7 @@ def get_image_from_texture2d( :return: PIL.Image object :rtype: Image """ + assert texture_2d.object_reader is not None return parse_image_data( texture_2d.get_image_data(), texture_2d.m_Width, From 0c24f4266ac2b80b49d516a9c7f81c59dd98ff53 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Wed, 28 May 2025 01:24:00 +0800 Subject: [PATCH 08/12] fix: Texture2DConverter - DXT swizzle --- UnityPy/export/Texture2DConverter.py | 61 ++++++++++++++++------------ 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 06fdec08..8cc7c75a 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -215,39 +215,44 @@ def image_to_texture2d( pil_mode = "RGB" # everything else defaulted to RGBA + switch_swizzle = None if platform == BuildTarget.Switch and platform_blob: gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) - s_tex_format = tex_format if tex_format == TextureFormat.RGB24: - s_tex_format = TextureFormat.RGBA32 + tex_format = TextureFormat.RGBA32 pil_mode = "RGBA" elif tex_format == TextureFormat.BGR24: - s_tex_format = TextureFormat.BGRA32 + tex_format = TextureFormat.BGRA32 pil_mode = "BGRA" - block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[s_tex_format] + block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP.get(tex_format) + if not block_size: + raise NotImplementedError( + f"Not implemented swizzle format: {tex_format.name}" + ) + width, height = TextureSwizzler.get_padded_texture_size( img.width, img.height, *block_size, gobs_per_block ) - img = pad_image(img, width, height) - img = Image.frombytes( - pil_mode, - img.size, - TextureSwizzler.swizzle( - img.tobytes("raw", pil_mode), width, height, *block_size, gobs_per_block - ), - ) + switch_swizzle = (block_size, gobs_per_block) + else: + width, height = get_compressed_image_size(img.width, img.height, tex_format) + img = pad_image(img, width, height) if compress_func: - width, height = get_compressed_image_size(img.width, img.height, tex_format) - img = pad_image(img, width, height) enc_img = compress_func( - img.tobytes("raw", "RGBA"), img.width, img.height, tex_format + img.tobytes("raw", pil_mode), img.width, img.height, tex_format ) else: enc_img = img.tobytes("raw", pil_mode) + if switch_swizzle is not None: + block_size, gobs_per_block = switch_swizzle + enc_img = bytes( + TextureSwizzler.swizzle(enc_img, width, height, *block_size, gobs_per_block) + ) + return enc_img, tex_format @@ -324,7 +329,12 @@ def parse_image_data( texture_format = texture_format pil_mode = "L" - block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[texture_format] + block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP.get(texture_format) + if not block_size: + raise NotImplementedError( + f"Not implemented swizzle format: {texture_format.name}" + ) + width, height = TextureSwizzler.get_padded_texture_size( width, height, *block_size, gobs_per_block ) @@ -343,21 +353,20 @@ def parse_image_data( else: image_data = texture2ddecoder.unpack_crunch(image_data) - conv_func = CONV_TABLE.get(texture_format) - if not conv_func: - raise NotImplementedError(f"Not implemented texture format: {texture_format}") - - img = conv_func(image_data, width, height) - if switch_swizzle is not None: block_size, gobs_per_block, pil_mode = switch_swizzle - swizzle_data = bytes( + image_data = bytes( TextureSwizzler.deswizzle( - img.tobytes("raw", pil_mode), width, height, *block_size, gobs_per_block + image_data, width, height, *block_size, gobs_per_block ) ) - img.mode = pil_mode - img = Image.frombytes(img.mode, (width, height), swizzle_data, "raw", pil_mode) + + conv_func = CONV_TABLE.get(texture_format) + if not conv_func: + raise NotImplementedError( + f"Not implemented texture format: {texture_format.name}" + ) + img = conv_func(image_data, width, height) if original_width != width or original_height != height: img = img.crop((0, 0, original_width, original_height)) From ae0d37f7c5fa28fc0cbde65b950b9277f5d7988b Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Wed, 28 May 2025 01:47:21 +0800 Subject: [PATCH 09/12] chore: Texture2DConverter - unify names --- UnityPy/export/Texture2DConverter.py | 108 ++++++++++++++------------- UnityPy/helpers/TextureSwizzler.py | 71 +++++++++--------- 2 files changed, 92 insertions(+), 87 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 8cc7c75a..8a3af8dd 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -9,15 +9,14 @@ import texture2ddecoder from PIL import Image -from ..enums import BuildTarget, TextureFormat +from ..enums import BuildTarget +from ..enums import TextureFormat as TF from ..helpers import TextureSwizzler if TYPE_CHECKING: from ..classes import Texture2D -TF = TextureFormat - TEXTURE_FORMAT_BLOCK_SIZE_TABLE: Dict[TF, Optional[Tuple[int, int]]] = {} for tf in TF: if tf.name.startswith("ASTC"): @@ -32,7 +31,7 @@ TEXTURE_FORMAT_BLOCK_SIZE_TABLE[tf] = block_size -def get_compressed_image_size(width: int, height: int, texture_format: TextureFormat): +def get_compressed_image_size(width: int, height: int, texture_format: TF): block_size = TEXTURE_FORMAT_BLOCK_SIZE_TABLE[texture_format] if block_size is None: return (width, height) @@ -88,7 +87,7 @@ def pad_image(img: Image.Image, pad_width: int, pad_height: int) -> Image.Image: def compress_etcpak( - data: bytes, width: int, height: int, target_texture_format: TextureFormat + data: bytes, width: int, height: int, target_texture_format: TF ) -> bytes: import etcpak @@ -115,7 +114,7 @@ def compress_etcpak( def compress_astc( - data: bytes, width: int, height: int, target_texture_format: TextureFormat + data: bytes, width: int, height: int, target_texture_format: TF ) -> bytes: astc_image = astc_encoder.ASTCImage( astc_encoder.ASTCType.U8, width, height, 1, data @@ -139,24 +138,24 @@ def image_to_texture2d( platform: int = 0, platform_blob: Optional[bytes] = None, flip: bool = True, -) -> Tuple[bytes, TextureFormat]: - if not isinstance(target_texture_format, TextureFormat): - target_texture_format = TextureFormat(target_texture_format) +) -> Tuple[bytes, TF]: + if not isinstance(target_texture_format, TF): + target_texture_format = TF(target_texture_format) if flip: img = img.transpose(Image.FLIP_TOP_BOTTOM) # defaults compress_func = None - tex_format = TF.RGBA32 + texture_format = TF.RGBA32 pil_mode = "RGBA" # DXT / BC if target_texture_format in [TF.DXT1, TF.DXT1Crunched]: - tex_format = TF.DXT1 + texture_format = TF.DXT1 compress_func = compress_etcpak elif target_texture_format in [TF.DXT5, TF.DXT5Crunched]: - tex_format = TF.DXT5 + texture_format = TF.DXT5 compress_func = compress_etcpak elif target_texture_format in [TF.BC4, TF.BC5, TF.BC7]: compress_func = compress_etcpak @@ -166,28 +165,32 @@ def image_to_texture2d( block_size = TEXTURE_FORMAT_BLOCK_SIZE_TABLE[target_texture_format] assert block_size is not None if img.mode == "RGB": - tex_format = getattr(TF, f"ASTC_RGB_{block_size[0]}x{block_size[1]}") + texture_format = getattr( + TF, f"ASTC_RGB_{block_size[0]}x{block_size[1]}" + ) else: - tex_format = getattr(TF, f"ASTC_RGBA_{block_size[0]}x{block_size[1]}") + texture_format = getattr( + TF, f"ASTC_RGBA_{block_size[0]}x{block_size[1]}" + ) else: - tex_format = target_texture_format + texture_format = target_texture_format compress_func = compress_astc # ETC elif target_texture_format in [TF.ETC_RGB4, TF.ETC_RGB4Crunched, TF.ETC_RGB4_3DS]: if target_texture_format == TF.ETC_RGB4_3DS: - tex_format = TF.ETC_RGB4_3DS + texture_format = TF.ETC_RGB4_3DS else: - tex_format = target_texture_format + texture_format = target_texture_format compress_func = compress_etcpak elif target_texture_format == TF.ETC2_RGB: - tex_format = TF.ETC2_RGB + texture_format = TF.ETC2_RGB compress_func = compress_etcpak elif target_texture_format in [TF.ETC2_RGBA8, TF.ETC2_RGBA8Crunched, TF.ETC2_RGBA1]: - tex_format = TF.ETC2_RGBA8 + texture_format = TF.ETC2_RGBA8 compress_func = compress_etcpak # L elif target_texture_format == TF.Alpha8: - tex_format = TF.Alpha8 + texture_format = TF.Alpha8 pil_mode = "L" # R - should probably be merged into #L, as pure R is used as Alpha # but need test data for this first @@ -199,7 +202,7 @@ def image_to_texture2d( TF.EAC_R, TF.EAC_R_SIGNED, ]: - tex_format = TF.R8 + texture_format = TF.R8 pil_mode = "R" # RGBA elif target_texture_format in [ @@ -211,7 +214,7 @@ def image_to_texture2d( TF.PVRTC_RGB4, TF.ATC_RGB4, ]: - tex_format = TF.RGB24 + texture_format = TF.RGB24 pil_mode = "RGB" # everything else defaulted to RGBA @@ -219,17 +222,17 @@ def image_to_texture2d( if platform == BuildTarget.Switch and platform_blob: gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) - if tex_format == TextureFormat.RGB24: - tex_format = TextureFormat.RGBA32 + if texture_format == TF.RGB24: + texture_format = TF.RGBA32 pil_mode = "RGBA" - elif tex_format == TextureFormat.BGR24: - tex_format = TextureFormat.BGRA32 + elif texture_format == TF.BGR24: + texture_format = TF.BGRA32 pil_mode = "BGRA" - block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP.get(tex_format) + block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP.get(texture_format) if not block_size: raise NotImplementedError( - f"Not implemented swizzle format: {tex_format.name}" + f"Not implemented swizzle format: {texture_format.name}" ) width, height = TextureSwizzler.get_padded_texture_size( @@ -237,26 +240,24 @@ def image_to_texture2d( ) switch_swizzle = (block_size, gobs_per_block) else: - width, height = get_compressed_image_size(img.width, img.height, tex_format) + width, height = get_compressed_image_size(img.width, img.height, texture_format) img = pad_image(img, width, height) if compress_func: enc_img = compress_func( - img.tobytes("raw", pil_mode), img.width, img.height, tex_format + img.tobytes("raw", pil_mode), img.width, img.height, texture_format ) else: enc_img = img.tobytes("raw", pil_mode) if switch_swizzle is not None: block_size, gobs_per_block = switch_swizzle - enc_img = bytes( - TextureSwizzler.swizzle(enc_img, width, height, *block_size, gobs_per_block) - ) + enc_img = TextureSwizzler.swizzle(enc_img, width, height, *block_size, gobs_per_block) - return enc_img, tex_format + return enc_img, texture_format -def assert_rgba(img: Image.Image, target_texture_format: TextureFormat) -> Image.Image: +def assert_rgba(img: Image.Image, target_texture_format: TF) -> Image.Image: if img.mode == "RGB": img = img.convert("RGBA") assert ( @@ -295,7 +296,7 @@ def parse_image_data( image_data: bytes, width: int, height: int, - texture_format: Union[int, TextureFormat], + texture_format: Union[int, TF], version: Tuple[int, int, int, int], platform: int, platform_blob: Optional[bytes] = None, @@ -308,8 +309,8 @@ def parse_image_data( if not image_data: raise ValueError("Texture2D has no image data") - if not isinstance(texture_format, TextureFormat): - texture_format = TextureFormat(texture_format) + if not isinstance(texture_format, TF): + texture_format = TF(texture_format) if platform == BuildTarget.XBOX360 and texture_format in XBOX_SWAP_FORMATS: image_data = swap_bytes_for_xbox(image_data) @@ -320,12 +321,12 @@ def parse_image_data( gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) pil_mode = "RGBA" - if texture_format == TextureFormat.RGB24: - texture_format = TextureFormat.RGBA32 - elif texture_format == TextureFormat.BGR24: - texture_format = TextureFormat.BGRA32 + if texture_format == TF.RGB24: + texture_format = TF.RGBA32 + elif texture_format == TF.BGR24: + texture_format = TF.BGRA32 pil_mode = "BGRA" - elif texture_format == TextureFormat.Alpha8: + elif texture_format == TF.Alpha8: texture_format = texture_format pil_mode = "L" @@ -355,11 +356,8 @@ def parse_image_data( if switch_swizzle is not None: block_size, gobs_per_block, pil_mode = switch_swizzle - image_data = bytes( - TextureSwizzler.deswizzle( - image_data, width, height, *block_size, gobs_per_block - ) - ) + image_data = TextureSwizzler.deswizzle(image_data, width, height, *block_size, gobs_per_block) + conv_func = CONV_TABLE.get(texture_format) if not conv_func: @@ -553,13 +551,17 @@ def rgb9e5float(image_data: bytes, width: int, height: int) -> Image.Image: return Image.frombytes("RGB", (width, height), rgb, "raw", "RGB") -CONV_TABLE: Dict[TextureFormat, Callable[[bytes, int, int], Image.Image]] = { +CONV_TABLE: Dict[TF, Callable[[bytes, int, int], Image.Image]] = { TF.Alpha8: lambda data, w, h: pillow(data, w, h, "L", "raw", "L"), - TF.ARGB4444: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;4B", (2, 1, 0, 3)), + TF.ARGB4444: lambda data, w, h: pillow( + data, w, h, "RGBA", "raw", "RGBA;4B", (2, 1, 0, 3) + ), TF.RGB24: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RGB"), TF.RGBA32: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA"), TF.ARGB32: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "ARGB"), - TF.ARGBFloat: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBAF", (2, 1, 0, 3)), + TF.ARGBFloat: lambda data, w, h: pillow( + data, w, h, "RGBA", "raw", "RGBAF", (2, 1, 0, 3) + ), TF.RGB565: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "BGR;16"), TF.BGR24: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "BGR"), TF.R8: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "R"), @@ -568,7 +570,9 @@ def rgb9e5float(image_data: bytes, width: int, height: int) -> Image.Image: TF.DXT1: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 1), TF.DXT3: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 2), TF.DXT5: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 3), - TF.RGBA4444: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;4B", (3, 2, 1, 0)), + TF.RGBA4444: lambda data, w, h: pillow( + data, w, h, "RGBA", "raw", "RGBA;4B", (3, 2, 1, 0) + ), TF.BGRA32: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "BGRA"), TF.RHalf: lambda data, w, h: half(data, w, h, "R", "raw", "R"), TF.RGHalf: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RGE"), diff --git a/UnityPy/helpers/TextureSwizzler.py b/UnityPy/helpers/TextureSwizzler.py index 0e622e8e..f818cee9 100644 --- a/UnityPy/helpers/TextureSwizzler.py +++ b/UnityPy/helpers/TextureSwizzler.py @@ -1,7 +1,7 @@ # based on https://github.com/nesrak1/UABEA/blob/master/TexturePlugin/Texture2DSwitchDeswizzler.cs from typing import Dict, Tuple -from ..enums import TextureFormat +from ..enums import TextureFormat as TF GOB_X_TEXEL_COUNT = 4 GOB_Y_TEXEL_COUNT = 8 @@ -24,7 +24,7 @@ def deswizzle( block_width: int, block_height: int, texels_per_block: int, -) -> bytearray: +) -> bytes: block_count_x = ceil_divide(width, block_width) block_count_y = ceil_divide(height, block_height) gob_count_x = block_count_x // GOB_X_TEXEL_COUNT @@ -46,7 +46,8 @@ def deswizzle( :TEXEL_BYTE_SIZE ] data_view = data_view[TEXEL_BYTE_SIZE:] - return new_data + + return bytes(new_data) def swizzle( @@ -56,7 +57,7 @@ def swizzle( block_width: int, block_height: int, texels_per_block: int, -) -> bytearray: +) -> bytes: block_count_x = ceil_divide(width, block_width) block_count_y = ceil_divide(height, block_height) gob_count_x = block_count_x // GOB_X_TEXEL_COUNT @@ -79,40 +80,40 @@ def swizzle( ] data_view = data_view[TEXEL_BYTE_SIZE:] - return new_data + return bytes(new_data) # this should be the amount of pixels that can fit 16 bytes -TEXTUREFORMAT_BLOCK_SIZE_MAP: Dict[TextureFormat, Tuple[int, int]] = { - TextureFormat.Alpha8: (16, 1), # 1 byte per pixel - TextureFormat.ARGB4444: (8, 1), # 2 bytes per pixel - TextureFormat.RGBA32: (4, 1), # 4 bytes per pixel - TextureFormat.ARGB32: (4, 1), # 4 bytes per pixel - TextureFormat.ARGBFloat: (1, 1), # 16 bytes per pixel (?) - TextureFormat.RGB565: (8, 1), # 2 bytes per pixel - TextureFormat.R16: (8, 1), # 2 bytes per pixel - TextureFormat.DXT1: (8, 4), # 8 bytes per 4x4=16 pixels - TextureFormat.DXT5: (4, 4), # 16 bytes per 4x4=16 pixels - TextureFormat.RGBA4444: (8, 1), # 2 bytes per pixel - TextureFormat.BGRA32: (4, 1), # 4 bytes per pixel - TextureFormat.BC6H: (4, 4), # 16 bytes per 4x4=16 pixels - TextureFormat.BC7: (4, 4), # 16 bytes per 4x4=16 pixels - TextureFormat.BC4: (8, 4), # 8 bytes per 4x4=16 pixels - TextureFormat.BC5: (4, 4), # 16 bytes per 4x4=16 pixels - TextureFormat.ASTC_RGB_4x4: (4, 4), # 16 bytes per 4x4=16 pixels - TextureFormat.ASTC_RGB_5x5: (5, 5), # 16 bytes per 5x5=25 pixels - TextureFormat.ASTC_RGB_6x6: (6, 6), # 16 bytes per 6x6=36 pixels - TextureFormat.ASTC_RGB_8x8: (8, 8), # 16 bytes per 8x8=64 pixels - TextureFormat.ASTC_RGB_10x10: (10, 10), # 16 bytes per 10x10=100 pixels - TextureFormat.ASTC_RGB_12x12: (12, 12), # 16 bytes per 12x12=144 pixels - TextureFormat.ASTC_RGBA_4x4: (4, 4), # 16 bytes per 4x4=16 pixels - TextureFormat.ASTC_RGBA_5x5: (5, 5), # 16 bytes per 5x5=25 pixels - TextureFormat.ASTC_RGBA_6x6: (6, 6), # 16 bytes per 6x6=36 pixels - TextureFormat.ASTC_RGBA_8x8: (8, 8), # 16 bytes per 8x8=64 pixels - TextureFormat.ASTC_RGBA_10x10: (10, 10), # 16 bytes per 10x10=100 pixels - TextureFormat.ASTC_RGBA_12x12: (12, 12), # 16 bytes per 12x12=144 pixels - TextureFormat.RG16: (8, 1), # 2 bytes per pixel - TextureFormat.R8: (16, 1), # 1 byte per pixel +TEXTUREFORMAT_BLOCK_SIZE_MAP: Dict[TF, Tuple[int, int]] = { + TF.Alpha8: (16, 1), # 1 byte per pixel + TF.ARGB4444: (8, 1), # 2 bytes per pixel + TF.RGBA32: (4, 1), # 4 bytes per pixel + TF.ARGB32: (4, 1), # 4 bytes per pixel + TF.ARGBFloat: (1, 1), # 16 bytes per pixel (?) + TF.RGB565: (8, 1), # 2 bytes per pixel + TF.R16: (8, 1), # 2 bytes per pixel + TF.DXT1: (8, 4), # 8 bytes per 4x4=16 pixels + TF.DXT5: (4, 4), # 16 bytes per 4x4=16 pixels + TF.RGBA4444: (8, 1), # 2 bytes per pixel + TF.BGRA32: (4, 1), # 4 bytes per pixel + TF.BC6H: (4, 4), # 16 bytes per 4x4=16 pixels + TF.BC7: (4, 4), # 16 bytes per 4x4=16 pixels + TF.BC4: (8, 4), # 8 bytes per 4x4=16 pixels + TF.BC5: (4, 4), # 16 bytes per 4x4=16 pixels + TF.ASTC_RGB_4x4: (4, 4), # 16 bytes per 4x4=16 pixels + TF.ASTC_RGB_5x5: (5, 5), # 16 bytes per 5x5=25 pixels + TF.ASTC_RGB_6x6: (6, 6), # 16 bytes per 6x6=36 pixels + TF.ASTC_RGB_8x8: (8, 8), # 16 bytes per 8x8=64 pixels + TF.ASTC_RGB_10x10: (10, 10), # 16 bytes per 10x10=100 pixels + TF.ASTC_RGB_12x12: (12, 12), # 16 bytes per 12x12=144 pixels + TF.ASTC_RGBA_4x4: (4, 4), # 16 bytes per 4x4=16 pixels + TF.ASTC_RGBA_5x5: (5, 5), # 16 bytes per 5x5=25 pixels + TF.ASTC_RGBA_6x6: (6, 6), # 16 bytes per 6x6=36 pixels + TF.ASTC_RGBA_8x8: (8, 8), # 16 bytes per 8x8=64 pixels + TF.ASTC_RGBA_10x10: (10, 10), # 16 bytes per 10x10=100 pixels + TF.ASTC_RGBA_12x12: (12, 12), # 16 bytes per 12x12=144 pixels + TF.RG16: (8, 1), # 2 bytes per pixel + TF.R8: (16, 1), # 1 byte per pixel } From 80bb79d7bc0ab532473b558da5f14d01492e9f0a Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Wed, 28 May 2025 10:58:30 +0800 Subject: [PATCH 10/12] fix: Texture2DConverter - clean up code --- UnityPy/export/Texture2DConverter.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 8a3af8dd..7206caba 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -53,10 +53,8 @@ def pad_image(img: Image.Image, pad_width: int, pad_height: int) -> Image.Image: if pad_width == ori_width and pad_height == ori_height: return img - pad_img = Image.new(img.mode, (pad_width, pad_height)) - pad_img.paste(img) - # Paste the original image at the top-left corner + pad_img = Image.new(img.mode, (pad_width, pad_height)) pad_img.paste(img, (0, 0)) # Fill the right border: duplicate the last column @@ -257,15 +255,6 @@ def image_to_texture2d( return enc_img, texture_format -def assert_rgba(img: Image.Image, target_texture_format: TF) -> Image.Image: - if img.mode == "RGB": - img = img.convert("RGBA") - assert ( - img.mode == "RGBA" - ), f"{target_texture_format} compression only supports RGB & RGBA images" # noqa: E501 - return img - - def get_image_from_texture2d( texture_2d: Texture2D, flip: bool = True, @@ -327,7 +316,6 @@ def parse_image_data( texture_format = TF.BGRA32 pil_mode = "BGRA" elif texture_format == TF.Alpha8: - texture_format = texture_format pil_mode = "L" block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP.get(texture_format) @@ -369,10 +357,7 @@ def parse_image_data( if original_width != width or original_height != height: img = img.crop((0, 0, original_width, original_height)) - if img and flip: - return img.transpose(Image.FLIP_TOP_BOTTOM) - - return img + return img.transpose(Image.FLIP_TOP_BOTTOM) if flip else img def swap_bytes_for_xbox(image_data: bytes) -> bytes: From 4e3ebc8c8012e5b14acccff595069ea0435f9d58 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Wed, 28 May 2025 11:05:13 +0800 Subject: [PATCH 11/12] refactor: Texture2DSwizzler - wrap methods --- UnityPy/export/Texture2DConverter.py | 68 ++++++--------- UnityPy/helpers/TextureSwizzler.py | 118 +++++++++++++++++++++------ 2 files changed, 116 insertions(+), 70 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index 7206caba..e9c6f64e 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -3,7 +3,7 @@ import struct from io import BytesIO from threading import Lock -from typing import TYPE_CHECKING, Callable, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Callable, Dict, Optional, Sequence, Tuple, Union import astc_encoder import texture2ddecoder @@ -17,6 +17,9 @@ from ..classes import Texture2D +PlatformBlobType = Union[bytes, Sequence[int]] + + TEXTURE_FORMAT_BLOCK_SIZE_TABLE: Dict[TF, Optional[Tuple[int, int]]] = {} for tf in TF: if tf.name.startswith("ASTC"): @@ -134,7 +137,7 @@ def image_to_texture2d( img: Image.Image, target_texture_format: Union[TF, int], platform: int = 0, - platform_blob: Optional[bytes] = None, + platform_blob: Optional[PlatformBlobType] = None, flip: bool = True, ) -> Tuple[bytes, TF]: if not isinstance(target_texture_format, TF): @@ -216,10 +219,8 @@ def image_to_texture2d( pil_mode = "RGB" # everything else defaulted to RGBA - switch_swizzle = None - if platform == BuildTarget.Switch and platform_blob: - gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) - + if TextureSwizzler.is_switch_swizzled(platform, platform_blob): + assert platform_blob is not None if texture_format == TF.RGB24: texture_format = TF.RGBA32 pil_mode = "RGBA" @@ -227,16 +228,9 @@ def image_to_texture2d( texture_format = TF.BGRA32 pil_mode = "BGRA" - block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP.get(texture_format) - if not block_size: - raise NotImplementedError( - f"Not implemented swizzle format: {texture_format.name}" - ) - - width, height = TextureSwizzler.get_padded_texture_size( - img.width, img.height, *block_size, gobs_per_block + width, height = TextureSwizzler.get_padded_image_size( + img.width, img.height, texture_format, platform_blob ) - switch_swizzle = (block_size, gobs_per_block) else: width, height = get_compressed_image_size(img.width, img.height, texture_format) @@ -248,9 +242,11 @@ def image_to_texture2d( else: enc_img = img.tobytes("raw", pil_mode) - if switch_swizzle is not None: - block_size, gobs_per_block = switch_swizzle - enc_img = TextureSwizzler.swizzle(enc_img, width, height, *block_size, gobs_per_block) + if TextureSwizzler.is_switch_swizzled(platform, platform_blob): + assert platform_blob is not None + enc_img = TextureSwizzler.swizzle( + enc_img, width, height, texture_format, platform_blob + ) return enc_img, texture_format @@ -285,10 +281,10 @@ def parse_image_data( image_data: bytes, width: int, height: int, - texture_format: Union[int, TF], + texture_format: Union[TF, int], version: Tuple[int, int, int, int], platform: int, - platform_blob: Optional[bytes] = None, + platform_blob: Optional[PlatformBlobType] = None, flip: bool = True, ) -> Image.Image: if not width or not height: @@ -304,30 +300,19 @@ def parse_image_data( if platform == BuildTarget.XBOX360 and texture_format in XBOX_SWAP_FORMATS: image_data = swap_bytes_for_xbox(image_data) - original_width, original_height = width, height - switch_swizzle = None - if platform == BuildTarget.Switch and platform_blob: - gobs_per_block = TextureSwizzler.get_switch_gobs_per_block(platform_blob) + ori_width, ori_height = width, height - pil_mode = "RGBA" + if TextureSwizzler.is_switch_swizzled(platform, platform_blob): + assert platform_blob is not None if texture_format == TF.RGB24: texture_format = TF.RGBA32 elif texture_format == TF.BGR24: texture_format = TF.BGRA32 - pil_mode = "BGRA" - elif texture_format == TF.Alpha8: - pil_mode = "L" - - block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP.get(texture_format) - if not block_size: - raise NotImplementedError( - f"Not implemented swizzle format: {texture_format.name}" - ) - width, height = TextureSwizzler.get_padded_texture_size( - width, height, *block_size, gobs_per_block + width, height = TextureSwizzler.get_padded_image_size( + width, height, texture_format, platform_blob ) - switch_swizzle = (block_size, gobs_per_block, pil_mode) + image_data = TextureSwizzler.deswizzle(image_data, width, height, texture_format, platform_blob) else: width, height = get_compressed_image_size(width, height, texture_format) @@ -342,11 +327,6 @@ def parse_image_data( else: image_data = texture2ddecoder.unpack_crunch(image_data) - if switch_swizzle is not None: - block_size, gobs_per_block, pil_mode = switch_swizzle - image_data = TextureSwizzler.deswizzle(image_data, width, height, *block_size, gobs_per_block) - - conv_func = CONV_TABLE.get(texture_format) if not conv_func: raise NotImplementedError( @@ -354,8 +334,8 @@ def parse_image_data( ) img = conv_func(image_data, width, height) - if original_width != width or original_height != height: - img = img.crop((0, 0, original_width, original_height)) + if ori_width != width or ori_height != height: + img = img.crop((0, 0, ori_width, ori_height)) return img.transpose(Image.FLIP_TOP_BOTTOM) if flip else img diff --git a/UnityPy/helpers/TextureSwizzler.py b/UnityPy/helpers/TextureSwizzler.py index f818cee9..6c9f6c8a 100644 --- a/UnityPy/helpers/TextureSwizzler.py +++ b/UnityPy/helpers/TextureSwizzler.py @@ -1,8 +1,14 @@ # based on https://github.com/nesrak1/UABEA/blob/master/TexturePlugin/Texture2DSwitchDeswizzler.cs -from typing import Dict, Tuple +from typing import Dict, Optional, Sequence, Tuple, Union + +from UnityPy.enums import BuildTarget from ..enums import TextureFormat as TF + +PlatformBlobType = Union[bytes, Sequence[int]] + + GOB_X_TEXEL_COUNT = 4 GOB_Y_TEXEL_COUNT = 8 TEXEL_BYTE_SIZE = 16 @@ -13,11 +19,11 @@ ] -def ceil_divide(a: int, b: int) -> int: +def _ceil_divide(a: int, b: int) -> int: return (a + b - 1) // b -def deswizzle( +def _deswizzle( data: bytes, width: int, height: int, @@ -25,8 +31,8 @@ def deswizzle( block_height: int, texels_per_block: int, ) -> bytes: - block_count_x = ceil_divide(width, block_width) - block_count_y = ceil_divide(height, block_height) + block_count_x = _ceil_divide(width, block_width) + block_count_y = _ceil_divide(height, block_height) gob_count_x = block_count_x // GOB_X_TEXEL_COUNT gob_count_y = block_count_y // GOB_Y_TEXEL_COUNT new_data = bytearray(len(data)) @@ -50,7 +56,7 @@ def deswizzle( return bytes(new_data) -def swizzle( +def _swizzle( data: bytes, width: int, height: int, @@ -58,8 +64,8 @@ def swizzle( block_height: int, texels_per_block: int, ) -> bytes: - block_count_x = ceil_divide(width, block_width) - block_count_y = ceil_divide(height, block_height) + block_count_x = _ceil_divide(width, block_width) + block_count_y = _ceil_divide(height, block_height) gob_count_x = block_count_x // GOB_X_TEXEL_COUNT gob_count_y = block_count_y // GOB_Y_TEXEL_COUNT new_data = bytearray(len(data)) @@ -83,8 +89,31 @@ def swizzle( return bytes(new_data) +def _get_padded_texture_size( + width: int, height: int, block_width: int, block_height: int, texels_per_block: int +) -> Tuple[int, int]: + width = ( + _ceil_divide(width, block_width * GOB_X_TEXEL_COUNT) + * block_width + * GOB_X_TEXEL_COUNT + ) + height = ( + _ceil_divide(height, block_height * GOB_Y_TEXEL_COUNT * texels_per_block) + * block_height + * GOB_Y_TEXEL_COUNT + * texels_per_block + ) + return width, height + + +def _get_texels_per_block(platform_blob: PlatformBlobType) -> int: + if not platform_blob: + raise ValueError("Given platform_blob is empty") + return 1 << int.from_bytes(platform_blob[8:12], "little") + + # this should be the amount of pixels that can fit 16 bytes -TEXTUREFORMAT_BLOCK_SIZE_MAP: Dict[TF, Tuple[int, int]] = { +TEXTURE_FORMAT_BLOCK_SIZE_MAP: Dict[TF, Tuple[int, int]] = { TF.Alpha8: (16, 1), # 1 byte per pixel TF.ARGB4444: (8, 1), # 2 bytes per pixel TF.RGBA32: (4, 1), # 4 bytes per pixel @@ -117,22 +146,59 @@ def swizzle( } -def get_padded_texture_size( - width: int, height: int, block_width: int, block_height: int, texels_per_block: int -): - width = ( - ceil_divide(width, block_width * GOB_X_TEXEL_COUNT) - * block_width - * GOB_X_TEXEL_COUNT - ) - height = ( - ceil_divide(height, block_height * GOB_Y_TEXEL_COUNT * texels_per_block) - * block_height - * GOB_Y_TEXEL_COUNT - * texels_per_block - ) - return width, height +def deswizzle( + data: bytes, + width: int, + height: int, + texture_format: TF, + platform_blob: PlatformBlobType, +) -> bytes: + block_size = TEXTURE_FORMAT_BLOCK_SIZE_MAP.get(texture_format) + if not block_size: + raise NotImplementedError( + f"Not implemented swizzle format: {texture_format.name}" + ) + texels_per_block = _get_texels_per_block(platform_blob) + return _deswizzle(data, width, height, *block_size, texels_per_block) -def get_switch_gobs_per_block(platform_blob: bytes) -> int: - return 1 << int.from_bytes(platform_blob[8:12], "little") +def swizzle( + data: bytes, + width: int, + height: int, + texture_format: TF, + platform_blob: PlatformBlobType, +) -> bytes: + block_size = TEXTURE_FORMAT_BLOCK_SIZE_MAP.get(texture_format) + if not block_size: + raise NotImplementedError( + f"Not implemented swizzle format: {texture_format.name}" + ) + texels_per_block = _get_texels_per_block(platform_blob) + return _swizzle(data, width, height, *block_size, texels_per_block) + + +def get_padded_image_size( + width: int, + height: int, + texture_format: TF, + platform_blob: PlatformBlobType, +): + block_size = TEXTURE_FORMAT_BLOCK_SIZE_MAP.get(texture_format) + if not block_size: + raise NotImplementedError( + f"Not implemented swizzle format: {texture_format.name}" + ) + texels_per_block = _get_texels_per_block(platform_blob) + return _get_padded_texture_size(width, height, *block_size, texels_per_block) + + +def is_switch_swizzled( + platform: Union[BuildTarget, int], platform_blob: Optional[PlatformBlobType] = None +) -> bool: + if platform != BuildTarget.Switch: + return False + if not platform_blob or len(platform_blob) < 12: + return False + gobs_per_block = _get_texels_per_block(platform_blob) + return gobs_per_block > 1 From 822c06b8a19ef0c48e6cc03bc4fc6465fe200f64 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Thu, 29 May 2025 01:32:01 +0800 Subject: [PATCH 12/12] refactor: Texture2DConverter - optimize `CONV_TABLE` again --- UnityPy/export/Texture2DConverter.py | 171 +++++++++++++-------------- 1 file changed, 83 insertions(+), 88 deletions(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index e9c6f64e..4c6fcf55 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -3,7 +3,7 @@ import struct from io import BytesIO from threading import Lock -from typing import TYPE_CHECKING, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Tuple, Union import astc_encoder import texture2ddecoder @@ -327,12 +327,12 @@ def parse_image_data( else: image_data = texture2ddecoder.unpack_crunch(image_data) - conv_func = CONV_TABLE.get(texture_format) - if not conv_func: + if texture_format not in CONV_TABLE: raise NotImplementedError( f"Not implemented texture format: {texture_format.name}" ) - img = conv_func(image_data, width, height) + conv_func, conv_args = CONV_TABLE[texture_format] + img = conv_func(image_data, width, height, *conv_args) if ori_width != width or ori_height != height: img = img.crop((0, 0, ori_width, ori_height)) @@ -516,90 +516,85 @@ def rgb9e5float(image_data: bytes, width: int, height: int) -> Image.Image: return Image.frombytes("RGB", (width, height), rgb, "raw", "RGB") -CONV_TABLE: Dict[TF, Callable[[bytes, int, int], Image.Image]] = { - TF.Alpha8: lambda data, w, h: pillow(data, w, h, "L", "raw", "L"), - TF.ARGB4444: lambda data, w, h: pillow( - data, w, h, "RGBA", "raw", "RGBA;4B", (2, 1, 0, 3) - ), - TF.RGB24: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RGB"), - TF.RGBA32: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA"), - TF.ARGB32: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "ARGB"), - TF.ARGBFloat: lambda data, w, h: pillow( - data, w, h, "RGBA", "raw", "RGBAF", (2, 1, 0, 3) - ), - TF.RGB565: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "BGR;16"), - TF.BGR24: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "BGR"), - TF.R8: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "R"), - TF.R16: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "R;16"), - TF.RG16: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RG"), - TF.DXT1: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 1), - TF.DXT3: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 2), - TF.DXT5: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 3), - TF.RGBA4444: lambda data, w, h: pillow( - data, w, h, "RGBA", "raw", "RGBA;4B", (3, 2, 1, 0) - ), - TF.BGRA32: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "BGRA"), - TF.RHalf: lambda data, w, h: half(data, w, h, "R", "raw", "R"), - TF.RGHalf: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RGE"), - TF.RGBAHalf: lambda data, w, h: half(data, w, h, "RGB", "raw", "RGB"), - TF.RFloat: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RF"), - TF.RGFloat: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RGF"), - TF.RGBAFloat: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBAF"), - # TF.YUY2: lambda data, w, h: NotImplementedError("YUY2 not implemented"), - TF.RGB9e5Float: lambda data, w, h: rgb9e5float(data, w, h), - TF.BC4: lambda data, w, h: pillow(data, w, h, "L", "bcn", 4), - TF.BC5: lambda data, w, h: pillow(data, w, h, "RGB", "bcn", 5), - TF.BC6H: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 6), - TF.BC7: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 7), - TF.DXT1Crunched: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 1), - TF.DXT5Crunched: lambda data, w, h: pillow(data, w, h, "RGBA", "bcn", 3), - TF.PVRTC_RGB2: lambda data, w, h: pvrtc(data, w, h, True), - TF.PVRTC_RGBA2: lambda data, w, h: pvrtc(data, w, h, True), - TF.PVRTC_RGB4: lambda data, w, h: pvrtc(data, w, h, False), - TF.PVRTC_RGBA4: lambda data, w, h: pvrtc(data, w, h, False), - TF.ETC_RGB4: lambda data, w, h: etc(data, w, h, "ETC1"), - TF.ATC_RGB4: lambda data, w, h: atc(data, w, h, False), - TF.ATC_RGBA8: lambda data, w, h: atc(data, w, h, True), - TF.EAC_R: lambda data, w, h: eac(data, w, h, "EAC_R"), - TF.EAC_R_SIGNED: lambda data, w, h: eac(data, w, h, "EAC_R_SIGNED"), - TF.EAC_RG: lambda data, w, h: eac(data, w, h, "EAC_RG"), - TF.EAC_RG_SIGNED: lambda data, w, h: eac(data, w, h, "EAC_RG_SIGNED"), - TF.ETC2_RGB: lambda data, w, h: etc(data, w, h, "ETC2_RGB"), - TF.ETC2_RGBA1: lambda data, w, h: etc(data, w, h, "ETC2_A1"), - TF.ETC2_RGBA8: lambda data, w, h: etc(data, w, h, "ETC2_A8"), - TF.ASTC_RGB_4x4: lambda data, w, h: astc(data, w, h, (4, 4)), - TF.ASTC_RGB_5x5: lambda data, w, h: astc(data, w, h, (5, 5)), - TF.ASTC_RGB_6x6: lambda data, w, h: astc(data, w, h, (6, 6)), - TF.ASTC_RGB_8x8: lambda data, w, h: astc(data, w, h, (8, 8)), - TF.ASTC_RGB_10x10: lambda data, w, h: astc(data, w, h, (10, 10)), - TF.ASTC_RGB_12x12: lambda data, w, h: astc(data, w, h, (12, 12)), - TF.ASTC_RGBA_4x4: lambda data, w, h: astc(data, w, h, (4, 4)), - TF.ASTC_RGBA_5x5: lambda data, w, h: astc(data, w, h, (5, 5)), - TF.ASTC_RGBA_6x6: lambda data, w, h: astc(data, w, h, (6, 6)), - TF.ASTC_RGBA_8x8: lambda data, w, h: astc(data, w, h, (8, 8)), - TF.ASTC_RGBA_10x10: lambda data, w, h: astc(data, w, h, (10, 10)), - TF.ASTC_RGBA_12x12: lambda data, w, h: astc(data, w, h, (12, 12)), - TF.ETC_RGB4_3DS: lambda data, w, h: etc(data, w, h, "ETC1"), - TF.ETC_RGBA8_3DS: lambda data, w, h: etc(data, w, h, "ETC1"), - TF.ETC_RGB4Crunched: lambda data, w, h: etc(data, w, h, "ETC1"), - TF.ETC2_RGBA8Crunched: lambda data, w, h: etc(data, w, h, "ETC2_A8"), - TF.ASTC_HDR_4x4: lambda data, w, h: astc(data, w, h, (4, 4)), - TF.ASTC_HDR_5x5: lambda data, w, h: astc(data, w, h, (5, 5)), - TF.ASTC_HDR_6x6: lambda data, w, h: astc(data, w, h, (6, 6)), - TF.ASTC_HDR_8x8: lambda data, w, h: astc(data, w, h, (8, 8)), - TF.ASTC_HDR_10x10: lambda data, w, h: astc(data, w, h, (10, 10)), - TF.ASTC_HDR_12x12: lambda data, w, h: astc(data, w, h, (12, 12)), - TF.RG32: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RG;16"), - TF.RGB48: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RGB;16"), - TF.RGBA64: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;16"), - TF.R8_SIGNED: lambda data, w, h: pillow(data, w, h, "R", "raw", "R;8s"), - TF.RG16_SIGNED: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RG;8s"), - TF.RGB24_SIGNED: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RGB;8s"), - TF.RGBA32_SIGNED: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;8s"), - TF.R16_SIGNED: lambda data, w, h: pillow(data, w, h, "R", "raw", "R;16s"), - TF.RG32_SIGNED: lambda data, w, h: rg(data, w, h, "RGB", "raw", "RG;16s"), - TF.RGB48_SIGNED: lambda data, w, h: pillow(data, w, h, "RGB", "raw", "RGB;16s"), - TF.RGBA64_SIGNED: lambda data, w, h: pillow(data, w, h, "RGBA", "raw", "RGBA;16s"), +# Mapping TextureFormat -> (converter function, (additional args, ...)) +CONV_TABLE: Dict[TF, Tuple[Callable[..., Image.Image], Tuple[Any, ...]]] = { + TF.Alpha8: (pillow, ("L", "raw", "L")), + TF.ARGB4444: (pillow, ("RGBA", "raw", "RGBA;4B", (2, 1, 0, 3))), + TF.RGB24: (pillow, ("RGB", "raw", "RGB")), + TF.RGBA32: (pillow, ("RGBA", "raw", "RGBA")), + TF.ARGB32: (pillow, ("RGBA", "raw", "ARGB")), + TF.ARGBFloat: (pillow, ("RGBA", "raw", "RGBAF", (2, 1, 0, 3))), + TF.RGB565: (pillow, ("RGB", "raw", "BGR;16")), + TF.BGR24: (pillow, ("RGB", "raw", "BGR")), + TF.R8: (pillow, ("RGB", "raw", "R")), + TF.R16: (pillow, ("RGB", "raw", "R;16")), + TF.RG16: (rg, ("RGB", "raw", "RG")), + TF.DXT1: (pillow, ("RGBA", "bcn", 1)), + TF.DXT3: (pillow, ("RGBA", "bcn", 2)), + TF.DXT5: (pillow, ("RGBA", "bcn", 3)), + TF.RGBA4444: (pillow, ("RGBA", "raw", "RGBA;4B", (3, 2, 1, 0))), + TF.BGRA32: (pillow, ("RGBA", "raw", "BGRA")), + TF.RHalf: (half, ("R", "raw", "R")), + TF.RGHalf: (rg, ("RGB", "raw", "RGE")), + TF.RGBAHalf: (half, ("RGB", "raw", "RGB")), + TF.RFloat: (pillow, ("RGB", "raw", "RF")), + TF.RGFloat: (rg, ("RGB", "raw", "RGF")), + TF.RGBAFloat: (pillow, ("RGBA", "raw", "RGBAF")), + # TF.YUY2: NotImplementedError("YUY2 not implemented"), + TF.RGB9e5Float: (rgb9e5float, ()), + TF.BC4: (pillow, ("L", "bcn", 4)), + TF.BC5: (pillow, ("RGB", "bcn", 5)), + TF.BC6H: (pillow, ("RGBA", "bcn", 6)), + TF.BC7: (pillow, ("RGBA", "bcn", 7)), + TF.DXT1Crunched: (pillow, ("RGBA", "bcn", 1)), + TF.DXT5Crunched: (pillow, ("RGBA", "bcn", 3)), + TF.PVRTC_RGB2: (pvrtc, (True,)), + TF.PVRTC_RGBA2: (pvrtc, (True,)), + TF.PVRTC_RGB4: (pvrtc, (False,)), + TF.PVRTC_RGBA4: (pvrtc, (False,)), + TF.ETC_RGB4: (etc, ("ETC1",)), + TF.ATC_RGB4: (atc, (False,)), + TF.ATC_RGBA8: (atc, (True,)), + TF.EAC_R: (eac, ("EAC_R",)), + TF.EAC_R_SIGNED: (eac, ("EAC_R_SIGNED",)), + TF.EAC_RG: (eac, ("EAC_RG",)), + TF.EAC_RG_SIGNED: (eac, ("EAC_RG_SIGNED",)), + TF.ETC2_RGB: (etc, ("ETC2_RGB",)), + TF.ETC2_RGBA1: (etc, ("ETC2_A1",)), + TF.ETC2_RGBA8: (etc, ("ETC2_A8",)), + TF.ASTC_RGB_4x4: (astc, ((4, 4))), + TF.ASTC_RGB_5x5: (astc, ((5, 5))), + TF.ASTC_RGB_6x6: (astc, ((6, 6))), + TF.ASTC_RGB_8x8: (astc, ((8, 8))), + TF.ASTC_RGB_10x10: (astc, ((10, 10))), + TF.ASTC_RGB_12x12: (astc, ((12, 12))), + TF.ASTC_RGBA_4x4: (astc, ((4, 4))), + TF.ASTC_RGBA_5x5: (astc, ((5, 5))), + TF.ASTC_RGBA_6x6: (astc, ((6, 6))), + TF.ASTC_RGBA_8x8: (astc, ((8, 8))), + TF.ASTC_RGBA_10x10: (astc, ((10, 10))), + TF.ASTC_RGBA_12x12: (astc, ((12, 12))), + TF.ETC_RGB4_3DS: (etc, ("ETC1",)), + TF.ETC_RGBA8_3DS: (etc, ("ETC1",)), + TF.ETC_RGB4Crunched: (etc, ("ETC1",)), + TF.ETC2_RGBA8Crunched: (etc, ("ETC2_A8",)), + TF.ASTC_HDR_4x4: (astc, ((4, 4))), + TF.ASTC_HDR_5x5: (astc, ((5, 5))), + TF.ASTC_HDR_6x6: (astc, ((6, 6))), + TF.ASTC_HDR_8x8: (astc, ((8, 8))), + TF.ASTC_HDR_10x10: (astc, ((10, 10))), + TF.ASTC_HDR_12x12: (astc, ((12, 12))), + TF.RG32: (rg, ("RGB", "raw", "RG;16")), + TF.RGB48: (pillow, ("RGB", "raw", "RGB;16")), + TF.RGBA64: (pillow, ("RGBA", "raw", "RGBA;16")), + TF.R8_SIGNED: (pillow, ("R", "raw", "R;8s")), + TF.RG16_SIGNED: (rg, ("RGB", "raw", "RG;8s")), + TF.RGB24_SIGNED: (pillow, ("RGB", "raw", "RGB;8s")), + TF.RGBA32_SIGNED: (pillow, ("RGBA", "raw", "RGBA;8s")), + TF.R16_SIGNED: (pillow, ("R", "raw", "R;16s")), + TF.RG32_SIGNED: (rg, ("RGB", "raw", "RG;16s")), + TF.RGB48_SIGNED: (pillow, ("RGB", "raw", "RGB;16s")), + TF.RGBA64_SIGNED: (pillow, ("RGBA", "raw", "RGBA;16s")), } # XBOX Swap Formats