Skip to content

Commit 0bb34ed

Browse files
committed
1.7.18 - "correct" Texture2D format selection for replacement & DXT/ETC compression added
1 parent ed883be commit 0bb34ed

3 files changed

Lines changed: 112 additions & 33 deletions

File tree

UnityPy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "1.7.17"
1+
__version__ = "1.7.18"
22

33
from .environment import Environment
44

UnityPy/classes/Texture2D.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,31 @@ def image(self, img):
1919
if img is None:
2020
raise Exception("No image provided")
2121

22-
if (isinstance(img, str) or isinstance(img, BufferedIOBase) or
23-
isinstance(img, RawIOBase) or isinstance(img, IOBase)):
22+
if (
23+
isinstance(img, str)
24+
or isinstance(img, BufferedIOBase)
25+
or isinstance(img, RawIOBase)
26+
or isinstance(img, IOBase)
27+
):
2428
img = Image.open(img)
2529

26-
img_data, tex_format = Texture2DConverter.image_to_texture2d(img)
30+
img_data, tex_format = Texture2DConverter.image_to_texture2d(
31+
img, self.m_TextureFormat
32+
)
2733
self.image_data = img_data
2834
# width * height * channel count
29-
self.m_CompleteImageSize = len(self._image_data)# img.width * img.height * len(img.getbands())
35+
self.m_CompleteImageSize = len(
36+
self._image_data
37+
) # img.width * img.height * len(img.getbands())
3038
self.m_TextureFormat = tex_format
3139

3240
@property
3341
def image_data(self):
3442
return self._image_data
3543

3644
def reset_streamdata(self):
37-
if not self.m_StreamData: return
45+
if not self.m_StreamData:
46+
return
3847
self.m_StreamData.offset = 0
3948
self.m_StreamData.size = 0
4049
self.m_StreamData.path = ""
@@ -43,7 +52,7 @@ def reset_streamdata(self):
4352
def image_data(self, data: bytes):
4453
self._image_data = data
4554
# prefer writing to cab if possible
46-
if self.m_StreamData:
55+
if self.version >= (5, 3) and self.m_StreamData.path:
4756
cab = self.assets_file.get_writeable_cab()
4857
if cab:
4958
self.m_StreamData.offset = cab.Position
@@ -53,20 +62,22 @@ def image_data(self, data: bytes):
5362
else:
5463
self.reset_streamdata()
5564

56-
def set_image(
57-
self, img, target_format: TextureFormat = None, in_cab: bool = False
58-
):
65+
def set_image(self, img, target_format: TextureFormat = None, in_cab: bool = False):
5966
if img is None:
6067
raise Exception("No image provided")
61-
img_data, tex_format = Texture2DConverter.image_to_texture2d(img)
68+
if not target_format:
69+
target_format = self.m_TextureFormat
70+
img_data, tex_format = Texture2DConverter.image_to_texture2d(img, target_format)
6271

6372
if in_cab:
6473
self.image_data = img_data
6574
else:
6675
self._image_data = img_data
6776
self.reset_streamdata()
6877
# width * height * channel count
69-
self.m_CompleteImageSize = len(self._image_data)#img.width * img.height * len(img.getbands())
78+
self.m_CompleteImageSize = len(
79+
self._image_data
80+
) # img.width * img.height * len(img.getbands())
7081
self.m_TextureFormat = tex_format
7182

7283
def __init__(self, reader):

UnityPy/export/Texture2DConverter.py

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import texture2ddecoder
2+
import etcpak
23
from PIL import Image
34
from copy import copy
45
from io import BytesIO
@@ -8,21 +9,70 @@
89
TF = TextureFormat
910

1011

11-
def image_to_texture2d(img: Image, flip: bool = True):
12-
# tex for eventually later usage of compressions
13-
14-
tex_format = None
12+
def image_to_texture2d(img: Image, target_texture_format: TF, flip: bool = True):
1513
if flip:
1614
img = img.transpose(Image.FLIP_TOP_BOTTOM)
1715

18-
if img.mode == "RGBA":
19-
tex_format = TextureFormat.RGBA32
20-
elif img.mode == "RGB":
21-
tex_format = TextureFormat.RGB24
22-
elif img.mode == "A":
23-
tex_format = TextureFormat.Alpha8
16+
# DXT
17+
if target_texture_format in [TF.DXT1, TF.DXT1Crunched]:
18+
raw_img = img.convert("RGBA").tobytes()
19+
enc_img = etcpak.compress_to_dxt1(raw_img, img.width, img.height)
20+
tex_format = TF.DXT1
21+
elif target_texture_format in [TF.DXT5, TF.DXT5Crunched]:
22+
raw_img = img.convert("RGBA").tobytes()
23+
enc_img = etcpak.compress_to_dxt5(raw_img, img.width, img.height)
24+
tex_format = TF.DXT5
25+
# ETC
26+
elif target_texture_format in [TF.ETC_RGB4, TF.ETC_RGB4Crunched, TF.ETC_RGB4_3DS]:
27+
r, g, b, a = img.split()
28+
raw_img = Image.merge("RGBA", (b, g, r, a)).tobytes()
29+
enc_img = etcpak.compress_to_etc1(raw_img, img.width, img.height)
30+
tex_format = TF.ETC_RGB4
31+
elif target_texture_format == TF.ETC2_RGB:
32+
r, g, b, a = img.split()
33+
raw_img = Image.merge("RGBA", (b, g, r, a)).tobytes()
34+
enc_img = etcpak.compress_to_etc2_rgb(raw_img, img.width, img.height)
35+
tex_format = TF.ETC2_RGB
36+
elif (
37+
target_texture_format in [TF.ETC2_RGBA8, TF.ETC2_RGBA8Crunched, TF.ETC2_RGBA1]
38+
or "_RGB_" in target_texture_format.name
39+
):
40+
r, g, b, a = img.split()
41+
raw_img = Image.merge("RGBA", (b, g, r, a)).tobytes()
42+
enc_img = etcpak.compress_to_etc2_rgba(raw_img, img.width, img.height)
43+
tex_format = TF.ETC2_RGBA8
44+
# A
45+
elif target_texture_format == TF.Alpha8:
46+
enc_img = Image.tobytes("raw", "A")
47+
tex_format = TF.Alpha8
48+
# R
49+
elif target_texture_format in [
50+
TF.R8,
51+
TF.R16,
52+
TF.RHalf,
53+
TF.RFloat,
54+
TF.EAC_R,
55+
TF.EAC_R_SIGNED,
56+
]:
57+
enc_img = Image.tobytes("raw", "R")
58+
tex_format = TF.R8
59+
# RGBA
60+
elif target_texture_format in [
61+
TF.RGB565,
62+
TF.RGB24,
63+
TF.RGB9e5Float,
64+
TF.PVRTC_RGB2,
65+
TF.PVRTC_RGB4,
66+
TF.ATC_RGB4,
67+
]:
68+
enc_img = Image.tobytes("raw", "RGB")
69+
tex_format = TF.RGB24
70+
# everything else defaulted to RGBA
71+
else:
72+
enc_img = img.tobytes("raw", "RGBA")
73+
tex_format = TF.RGBA32
2474

25-
return img.tobytes(), tex_format
75+
return enc_img, tex_format
2676

2777

2878
def get_image_from_texture2d(texture_2d, flip=True) -> Image:
@@ -38,9 +88,12 @@ def get_image_from_texture2d(texture_2d, flip=True) -> Image:
3888
image_data = copy(bytes(texture_2d.image_data))
3989
if not image_data:
4090
return Image.new("RGB", (0, 0))
41-
42-
texture_format = texture_2d.m_TextureFormat if isinstance(texture_2d, TextureFormat) else TextureFormat(
43-
texture_2d.m_TextureFormat)
91+
92+
texture_format = (
93+
texture_2d.m_TextureFormat
94+
if isinstance(texture_2d.m_TextureFormat, TF)
95+
else TF(texture_2d.m_TextureFormat)
96+
)
4497
selection = CONV_TABLE[texture_format]
4598

4699
if len(selection) == 0:
@@ -53,9 +106,12 @@ def get_image_from_texture2d(texture_2d, flip=True) -> Image:
53106

54107
if "Crunched" in texture_format.name:
55108
version = texture_2d.version
56-
if (version[0] > 2017 or (version[0] == 2017 and version[1] >= 3) # 2017.3 and up
57-
or texture_format == TextureFormat.ETC_RGB4Crunched
58-
or texture_format == TextureFormat.ETC2_RGBA8Crunched):
109+
if (
110+
version[0] > 2017
111+
or (version[0] == 2017 and version[1] >= 3) # 2017.3 and up
112+
or texture_format == TF.ETC_RGB4Crunched
113+
or texture_format == TF.ETC2_RGBA8Crunched
114+
):
59115
image_data = texture2ddecoder.unpack_unity_crunch(image_data)
60116
else:
61117
image_data = texture2ddecoder.unpack_crunch(image_data)
@@ -81,15 +137,21 @@ def swap_bytes_for_xbox(image_data: bytes, build_target: BuildTarget) -> bytes:
81137
:rtype: bytes
82138
"""
83139
if (
84-
build_target == BuildTarget.XBOX360
140+
build_target == BuildTarget.XBOX360
85141
): # swap bytes for Xbox confirmed,PS3 not encountered
86142
for i in range(0, len(image_data), 2):
87-
image_data[i: i + 2] = image_data[i: i + 2][::-1]
143+
image_data[i : i + 2] = image_data[i : i + 2][::-1]
88144
return image_data
89145

90146

91147
def pillow(
92-
image_data: bytes, width: int, height: int, mode: str, codec: str, args, swap: tuple = None
148+
image_data: bytes,
149+
width: int,
150+
height: int,
151+
mode: str,
152+
codec: str,
153+
args,
154+
swap: tuple = None,
93155
) -> Image:
94156
img = (
95157
Image.frombytes(mode, (width, height), image_data, codec, args)
@@ -149,7 +211,13 @@ def eac(image_data: bytes, width: int, height: int, fmt: list):
149211

150212

151213
def half(
152-
image_data: bytes, width: int, height: int, mode: str, codec: str, args, swap: tuple = None
214+
image_data: bytes,
215+
width: int,
216+
height: int,
217+
mode: str,
218+
codec: str,
219+
args,
220+
swap: tuple = None,
153221
) -> Image:
154222
# convert half-float to int8
155223
stream = BytesIO(image_data)

0 commit comments

Comments
 (0)