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 3068a6a2..4c6fcf55 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -1,23 +1,24 @@ -from __future__ import annotations +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 +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Tuple, Union import astc_encoder 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 +PlatformBlobType = Union[bytes, Sequence[int]] + TEXTURE_FORMAT_BLOCK_SIZE_TABLE: Dict[TF, Optional[Tuple[int, int]]] = {} for tf in TF: @@ -33,7 +34,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) @@ -55,10 +56,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 @@ -89,7 +88,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 @@ -116,15 +115,15 @@ 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 ) 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) @@ -138,35 +137,28 @@ 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, 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 + # 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 - 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 + texture_format = TF.DXT5 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"): @@ -174,30 +166,34 @@ 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 - # 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 + 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 elif target_texture_format in [ TF.R8, @@ -207,7 +203,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 [ @@ -219,50 +215,40 @@ 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 - if platform == BuildTarget.Switch and platform_blob is not None: - gobsPerBlock = TextureSwizzler.get_switch_gobs_per_block(platform_blob) - s_tex_format = tex_format - if tex_format == TextureFormat.RGB24: - s_tex_format = TextureFormat.RGBA32 + 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" - # elif tex_format == TextureFormat.BGR24: - # s_tex_format = TextureFormat.BGRA32 - 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", - img.size, - TextureSwizzler.swizzle( - img.tobytes("raw", "RGBA"), width, height, *block_size, gobsPerBlock - ), + elif texture_format == TF.BGR24: + texture_format = TF.BGRA32 + pil_mode = "BGRA" + + width, height = TextureSwizzler.get_padded_image_size( + img.width, img.height, texture_format, platform_blob ) + else: + width, height = get_compressed_image_size(img.width, img.height, texture_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, texture_format ) else: enc_img = img.tobytes("raw", pil_mode) - return enc_img, tex_format - + 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 + ) -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 - return img + return enc_img, texture_format def get_image_from_texture2d( @@ -278,6 +264,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, @@ -294,48 +281,42 @@ def parse_image_data( image_data: bytes, width: int, height: int, - texture_format: Union[int, TextureFormat], + 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: 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") - 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) - 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) - if texture_format == TextureFormat.RGB24: - texture_format = TextureFormat.RGBA32 - elif texture_format == TextureFormat.BGR24: - texture_format = TextureFormat.BGRA32 - block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[texture_format] - width, height = TextureSwizzler.get_padded_texture_size( - width, height, *block_size, gobsPerBlock + ori_width, ori_height = width, height + + 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 + + width, height = TextureSwizzler.get_padded_image_size( + width, height, texture_format, platform_blob ) - switch_swizzle = (block_size, gobsPerBlock) + image_data = TextureSwizzler.deswizzle(image_data, width, height, texture_format, platform_blob) else: width, height = get_compressed_image_size(width, height, texture_format) - selection = CONV_TABLE[texture_format] - - if len(selection) == 0: - 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 @@ -346,30 +327,25 @@ def parse_image_data( else: image_data = texture2ddecoder.unpack_crunch(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 + if texture_format not in CONV_TABLE: + raise NotImplementedError( + f"Not implemented texture format: {texture_format.name}" ) - img = Image.frombytes(img.mode, (width, height), image_data, "raw", "RGBA") + conv_func, conv_args = CONV_TABLE[texture_format] + img = conv_func(image_data, width, height, *conv_args) - 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)) - 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: - """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( @@ -450,22 +426,22 @@ 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") -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": @@ -474,6 +450,9 @@ def eac(image_data: bytes, width: int, height: int, fmt: list) -> 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") @@ -537,90 +516,86 @@ 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, "RGBA", "raw", "A"), - (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"), +# 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")), } -# 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] diff --git a/UnityPy/helpers/TextureSwizzler.py b/UnityPy/helpers/TextureSwizzler.py index 0e622e8e..6c9f6c8a 100644 --- a/UnityPy/helpers/TextureSwizzler.py +++ b/UnityPy/helpers/TextureSwizzler.py @@ -1,7 +1,13 @@ # 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]] -from ..enums import TextureFormat GOB_X_TEXEL_COUNT = 4 GOB_Y_TEXEL_COUNT = 8 @@ -13,20 +19,20 @@ ] -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, block_width: int, block_height: int, texels_per_block: int, -) -> bytearray: - block_count_x = ceil_divide(width, block_width) - block_count_y = ceil_divide(height, block_height) +) -> 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 gob_count_y = block_count_y // GOB_Y_TEXEL_COUNT new_data = bytearray(len(data)) @@ -46,19 +52,20 @@ def deswizzle( :TEXEL_BYTE_SIZE ] data_view = data_view[TEXEL_BYTE_SIZE:] - return new_data + return bytes(new_data) -def swizzle( + +def _swizzle( data: bytes, width: int, height: int, block_width: int, block_height: int, texels_per_block: int, -) -> bytearray: - block_count_x = ceil_divide(width, block_width) - block_count_y = ceil_divide(height, block_height) +) -> 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 gob_count_y = block_count_y // GOB_Y_TEXEL_COUNT new_data = bytearray(len(data)) @@ -79,53 +86,19 @@ 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 -} - -def get_padded_texture_size( +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) + _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) + _ceil_divide(height, block_height * GOB_Y_TEXEL_COUNT * texels_per_block) * block_height * GOB_Y_TEXEL_COUNT * texels_per_block @@ -133,5 +106,99 @@ def get_padded_texture_size( return width, height -def get_switch_gobs_per_block(platform_blob: bytes) -> int: +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 +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 + 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 +} + + +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 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