Skip to content

Commit fe301d6

Browse files
committed
Texture2D - add padding handling for Textures and improve swizzle support
1 parent 59586b5 commit fe301d6

File tree

2 files changed

+223
-70
lines changed

2 files changed

+223
-70
lines changed

UnityPy/classes/legacy_patch/Texture2D.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ def _Texture2d_set_image(
2525
if not isinstance(img, Image.Image):
2626
img = Image.open(img)
2727

28-
img_data, tex_format = Texture2DConverter.image_to_texture2d(img, target_format)
28+
platform = self.object_reader.platform if self.object_reader is not None else 0
29+
img_data, tex_format = Texture2DConverter.image_to_texture2d(
30+
img, target_format, platform, self.m_PlatformBlob
31+
)
2932
self.m_Width = img.width
3033
self.m_Height = img.height
3134

UnityPy/export/Texture2DConverter.py

Lines changed: 219 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -19,80 +19,184 @@
1919

2020
TF = TextureFormat
2121

22+
TEXTURE_FORMAT_BLOCK_SIZE_TABLE: Dict[TF, Optional[Tuple[int, int]]] = {}
23+
for tf in TF:
24+
if tf.name.startswith("ASTC"):
25+
split = tf.name.rsplit("_", 1)[1].split("x")
26+
block_size = (int(split[0]), int(split[1]))
27+
elif tf.name.startswith(("DXT", "BC", "ETC", "EAC")):
28+
block_size = (4, 4)
29+
elif tf.name.startswith("PVRTC"):
30+
block_size = (8 if tf.name.endswith("2") else 4, 4)
31+
else:
32+
block_size = None
33+
TEXTURE_FORMAT_BLOCK_SIZE_TABLE[tf] = block_size
34+
35+
36+
def get_compressed_image_size(width: int, height: int, texture_format: TextureFormat):
37+
block_size = TEXTURE_FORMAT_BLOCK_SIZE_TABLE[texture_format]
38+
if block_size is None:
39+
return (width, height)
40+
block_width, block_height = block_size
41+
42+
def pad(value: int, pad_by: int) -> int:
43+
to_pad = value % pad_by
44+
if to_pad:
45+
value += pad_by - to_pad
46+
return value
47+
48+
width = pad(width, block_width)
49+
height = pad(height, block_height)
50+
return width, height
51+
52+
53+
def pad_image(img: Image.Image, pad_width: int, pad_height: int) -> Image.Image:
54+
ori_width, ori_height = img.size
55+
if pad_width == ori_width and pad_height == ori_height:
56+
return img
57+
58+
pad_img = Image.new(img.mode, (pad_width, pad_height))
59+
pad_img.paste(img)
60+
61+
# Paste the original image at the top-left corner
62+
pad_img.paste(img, (0, 0))
63+
64+
# Fill the right border: duplicate the last column
65+
if pad_width != ori_width:
66+
right_strip = img.crop((ori_width - 1, 0, ori_width, ori_height))
67+
right_strip = right_strip.resize(
68+
(pad_width - ori_width, ori_height), resample=Image.NEAREST
69+
)
70+
pad_img.paste(right_strip, (ori_width, 0))
71+
72+
# Fill the bottom border: duplicate the last row
73+
if pad_height != ori_height:
74+
bottom_strip = img.crop((0, ori_height - 1, ori_width, ori_height))
75+
bottom_strip = bottom_strip.resize(
76+
(ori_width, pad_height - ori_height), resample=Image.NEAREST
77+
)
78+
pad_img.paste(bottom_strip, (0, ori_height))
79+
80+
# Fill the bottom-right corner with the bottom-right pixel
81+
if pad_width != ori_width and pad_height != ori_height:
82+
corner = img.getpixel((ori_width - 1, ori_height - 1))
83+
corner_img = Image.new(
84+
img.mode, (pad_width - ori_width, pad_height - ori_height), color=corner
85+
)
86+
pad_img.paste(corner_img, (ori_width, ori_height))
87+
88+
return pad_img
89+
90+
91+
def compress_etcpak(
92+
data: bytes, width: int, height: int, target_texture_format: TextureFormat
93+
) -> bytes:
94+
import etcpak
95+
96+
if target_texture_format in [TF.DXT1, TF.DXT1Crunched]:
97+
return etcpak.compress_bc1(data, width, height)
98+
elif target_texture_format in [TF.DXT5, TF.DXT5Crunched]:
99+
return etcpak.compress_bc3(data, width, height)
100+
elif target_texture_format == TF.BC4:
101+
return etcpak.compress_bc4(data, width, height)
102+
elif target_texture_format == TF.BC5:
103+
return etcpak.compress_bc5(data, width, height)
104+
elif target_texture_format == TF.BC7:
105+
return etcpak.compress_bc7(data, width, height, None)
106+
elif target_texture_format in [TF.ETC_RGB4, TF.ETC_RGB4Crunched, TF.ETC_RGB4_3DS]:
107+
return etcpak.compress_etc1_rgb(data, width, height)
108+
elif target_texture_format == TF.ETC2_RGB:
109+
return etcpak.compress_etc2_rgb(data, width, height)
110+
elif target_texture_format in [TF.ETC2_RGBA8, TF.ETC2_RGBA8Crunched, TF.ETC2_RGBA1]:
111+
return etcpak.compress_etc2_rgba(data, width, height)
112+
else:
113+
raise NotImplementedError(
114+
f"etcpak has no compress function for {target_texture_format.name}"
115+
)
116+
117+
118+
def compress_astc(
119+
data: bytes, width: int, height: int, target_texture_format: TextureFormat
120+
) -> bytes:
121+
astc_image = astc_encoder.ASTCImage(
122+
astc_encoder.ASTCType.U8, width, height, 1, data
123+
)
124+
block_size = TEXTURE_FORMAT_BLOCK_SIZE_TABLE[target_texture_format]
125+
assert block_size is not None, (
126+
f"failed to get block size for {target_texture_format.name}"
127+
)
128+
swizzle = astc_encoder.ASTCSwizzle.from_str("RGBA")
129+
130+
context, lock = get_astc_context(block_size)
131+
with lock:
132+
enc_img = context.compress(astc_image, swizzle)
133+
134+
return enc_img
135+
22136

23137
def image_to_texture2d(
24-
img: Image.Image, target_texture_format: Union[TF, int], flip: bool = True
138+
img: Image.Image,
139+
target_texture_format: Union[TF, int],
140+
platform: int = 0,
141+
platform_blob: Optional[bytes] = None,
142+
flip: bool = True,
25143
) -> Tuple[bytes, TextureFormat]:
26-
if isinstance(target_texture_format, int):
144+
if not isinstance(target_texture_format, TextureFormat):
27145
target_texture_format = TextureFormat(target_texture_format)
28146

29-
import etcpak
30-
31147
if flip:
32148
img = img.transpose(Image.FLIP_TOP_BOTTOM)
33149

150+
# defaults
151+
compress_func = None
152+
tex_format = TF.RGBA32
153+
pil_mode = "RGBA"
154+
34155
# DXT
35156
if target_texture_format in [TF.DXT1, TF.DXT1Crunched]:
36-
raw_img = img.tobytes("raw", "RGBA")
37-
enc_img = etcpak.compress_bc1(raw_img, img.width, img.height)
38157
tex_format = TF.DXT1
158+
compress_func = compress_etcpak
39159
elif target_texture_format in [TF.DXT5, TF.DXT5Crunched]:
40-
raw_img = img.tobytes("raw", "RGBA")
41-
enc_img = etcpak.compress_bc3(raw_img, img.width, img.height)
42160
tex_format = TF.DXT5
161+
compress_func = compress_etcpak
43162
elif target_texture_format in [TF.BC4]:
44-
raw_img = img.tobytes("raw", "RGBA")
45-
enc_img = etcpak.compress_bc4(raw_img, img.width, img.height)
46163
tex_format = TF.BC4
164+
compress_func = compress_etcpak
47165
elif target_texture_format in [TF.BC5]:
48-
raw_img = img.tobytes("raw", "RGBA")
49-
enc_img = etcpak.compress_bc5(raw_img, img.width, img.height)
50166
tex_format = TF.BC5
167+
compress_func = compress_etcpak
51168
elif target_texture_format in [TF.BC7]:
52-
raw_img = img.tobytes("raw", "RGBA")
53-
enc_img = etcpak.compress_bc7(raw_img, img.width, img.height)
54169
tex_format = TF.BC7
170+
compress_func = compress_etcpak
171+
# ASTC
172+
elif target_texture_format.name.startswith("ASTC"):
173+
if "_HDR_" in target_texture_format.name:
174+
block_size = TEXTURE_FORMAT_BLOCK_SIZE_TABLE[target_texture_format]
175+
assert block_size is not None
176+
if img.mode == "RGB":
177+
tex_format = getattr(TF, f"ASTC_RGB_{block_size[0]}x{block_size[1]}")
178+
else:
179+
tex_format = getattr(TF, f"ASTC_RGBA_{block_size[0]}x{block_size[1]}")
180+
else:
181+
tex_format = target_texture_format
182+
compress_func = compress_astc
55183
# ETC
56184
elif target_texture_format in [TF.ETC_RGB4, TF.ETC_RGB4Crunched, TF.ETC_RGB4_3DS]:
57-
raw_img = img.tobytes("raw", "RGBA")
58-
enc_img = etcpak.compress_etc1_rgb(raw_img, img.width, img.height)
59-
tex_format = TF.ETC_RGB4
185+
if target_texture_format == TF.ETC_RGB4_3DS:
186+
tex_format = TF.ETC_RGB4_3DS
187+
else:
188+
tex_format = target_texture_format
189+
compress_func = compress_etcpak
60190
elif target_texture_format == TF.ETC2_RGB:
61-
raw_img = img.tobytes("raw", "RGBA")
62-
enc_img = etcpak.compress_etc2_rgb(raw_img, img.width, img.height)
63191
tex_format = TF.ETC2_RGB
64-
elif (
65-
target_texture_format in [TF.ETC2_RGBA8, TF.ETC2_RGBA8Crunched, TF.ETC2_RGBA1]
66-
or "_RGB_" in target_texture_format.name
67-
):
68-
raw_img = img.tobytes("raw", "RGBA")
69-
enc_img = etcpak.compress_etc2_rgba(raw_img, img.width, img.height)
192+
compress_func = compress_etcpak
193+
elif target_texture_format in [TF.ETC2_RGBA8, TF.ETC2_RGBA8Crunched, TF.ETC2_RGBA1]:
70194
tex_format = TF.ETC2_RGBA8
71-
# ASTC
72-
elif target_texture_format.name.startswith("ASTC"):
73-
raw_img = img.tobytes("raw", "RGBA")
74-
raw_img = astc_encoder.ASTCImage(
75-
astc_encoder.ASTCType.U8, img.width, img.height, 1, raw_img
76-
)
77-
block_size = tuple(
78-
map(int, target_texture_format.name.rsplit("_", 1)[1].split("x"))
79-
)
80-
if img.mode == "RGB":
81-
tex_format = getattr(TF, f"ASTC_RGB_{block_size[0]}x{block_size[1]}")
82-
else:
83-
tex_format = getattr(TF, f"ASTC_RGBA_{block_size[0]}x{block_size[1]}")
84-
85-
swizzle = astc_encoder.ASTCSwizzle.from_str("RGBA")
86-
87-
context, lock = get_astc_context(block_size)
88-
with lock:
89-
enc_img = context.compress(raw_img, swizzle)
90-
91-
tex_format = target_texture_format
195+
compress_func = compress_etcpak
92196
# A
93197
elif target_texture_format == TF.Alpha8:
94-
enc_img = img.tobytes("raw", "A")
95198
tex_format = TF.Alpha8
199+
pil_mode = "A"
96200
# R - should probably be moerged into #A, as pure R is used as Alpha
97201
# but need test data for this first
98202
elif target_texture_format in [
@@ -103,33 +207,61 @@ def image_to_texture2d(
103207
TF.EAC_R,
104208
TF.EAC_R_SIGNED,
105209
]:
106-
enc_img = img.tobytes("raw", "R")
107210
tex_format = TF.R8
211+
pil_mode = "R"
108212
# RGBA
109213
elif target_texture_format in [
110214
TF.RGB565,
111215
TF.RGB24,
216+
TF.BGR24,
112217
TF.RGB9e5Float,
113218
TF.PVRTC_RGB2,
114219
TF.PVRTC_RGB4,
115220
TF.ATC_RGB4,
116221
]:
117-
enc_img = img.tobytes("raw", "RGB")
118222
tex_format = TF.RGB24
223+
pil_mode = "RGB"
119224
# everything else defaulted to RGBA
225+
226+
if platform == BuildTarget.Switch and platform_blob is not None:
227+
gobsPerBlock = TextureSwizzler.get_switch_gobs_per_block(platform_blob)
228+
s_tex_format = tex_format
229+
if tex_format == TextureFormat.RGB24:
230+
s_tex_format = TextureFormat.RGBA32
231+
pil_mode = "RGBA"
232+
# elif tex_format == TextureFormat.BGR24:
233+
# s_tex_format = TextureFormat.BGRA32
234+
block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[s_tex_format]
235+
width, height = TextureSwizzler.get_padded_texture_size(
236+
img.width, img.height, *block_size, gobsPerBlock
237+
)
238+
img = pad_image(img, width, height)
239+
img = Image.frombytes(
240+
"RGBA",
241+
img.size,
242+
TextureSwizzler.swizzle(
243+
img.tobytes("raw", "RGBA"), width, height, *block_size, gobsPerBlock
244+
),
245+
)
246+
247+
if compress_func:
248+
width, height = get_compressed_image_size(img.width, img.height, tex_format)
249+
img = pad_image(img, width, height)
250+
enc_img = compress_func(
251+
img.tobytes("raw", "RGBA"), img.width, img.height, tex_format
252+
)
120253
else:
121-
enc_img = img.tobytes("raw", "RGBA")
122-
tex_format = TF.RGBA32
254+
enc_img = img.tobytes("raw", pil_mode)
123255

124256
return enc_img, tex_format
125257

126258

127259
def assert_rgba(img: Image.Image, target_texture_format: TextureFormat) -> Image.Image:
128260
if img.mode == "RGB":
129261
img = img.convert("RGBA")
130-
assert (
131-
img.mode == "RGBA"
132-
), f"{target_texture_format} compression only supports RGB & RGBA images" # noqa: E501
262+
assert img.mode == "RGBA", (
263+
f"{target_texture_format} compression only supports RGB & RGBA images"
264+
) # noqa: E501
133265
return img
134266

135267

@@ -163,36 +295,45 @@ def parse_image_data(
163295
width: int,
164296
height: int,
165297
texture_format: Union[int, TextureFormat],
166-
version: tuple,
298+
version: Tuple[int, int, int, int],
167299
platform: int,
168300
platform_blob: Optional[bytes] = None,
169301
flip: bool = True,
170302
) -> Image.Image:
303+
if not width or not height:
304+
return Image.new("RGBA", (0, 0))
305+
171306
image_data = copy(bytes(image_data))
172307
if not image_data:
173308
raise ValueError("Texture2D has no image data")
174309

175-
selection = CONV_TABLE[texture_format]
176-
177-
if len(selection) == 0:
178-
raise NotImplementedError(
179-
f"Not implemented texture format: {texture_format}"
180-
)
310+
if not isinstance(texture_format, TextureFormat):
311+
texture_format = TextureFormat(texture_format)
181312

182313
if platform == BuildTarget.XBOX360 and texture_format in XBOX_SWAP_FORMATS:
183314
image_data = swap_bytes_for_xbox(image_data)
184-
elif platform == BuildTarget.Switch and platform_blob is not None:
315+
316+
original_width, original_height = (width, height)
317+
switch_swizzle = None
318+
if platform == BuildTarget.Switch and platform_blob is not None:
185319
gobsPerBlock = TextureSwizzler.get_switch_gobs_per_block(platform_blob)
320+
if texture_format == TextureFormat.RGB24:
321+
texture_format = TextureFormat.RGBA32
322+
elif texture_format == TextureFormat.BGR24:
323+
texture_format = TextureFormat.BGRA32
186324
block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[texture_format]
187-
padded_size = TextureSwizzler.get_padded_texture_size(
325+
width, height = TextureSwizzler.get_padded_texture_size(
188326
width, height, *block_size, gobsPerBlock
189327
)
190-
image_data = TextureSwizzler.deswizzle(
191-
image_data, *padded_size, *block_size, gobsPerBlock
192-
)
328+
switch_swizzle = (block_size, gobsPerBlock)
329+
else:
330+
width, height = get_compressed_image_size(width, height, texture_format)
331+
332+
selection = CONV_TABLE[texture_format]
333+
334+
if len(selection) == 0:
335+
raise NotImplementedError(f"Not implemented texture format: {texture_format}")
193336

194-
if not isinstance(texture_format, TextureFormat):
195-
texture_format = TextureFormat(texture_format)
196337
if "Crunched" in texture_format.name:
197338
version = version
198339
if (
@@ -207,6 +348,15 @@ def parse_image_data(
207348

208349
img = selection[0](image_data, width, height, *selection[1:])
209350

351+
if switch_swizzle is not None:
352+
image_data = TextureSwizzler.deswizzle(
353+
img.tobytes("raw", "RGBA"), width, height, *block_size, gobsPerBlock
354+
)
355+
img = Image.frombytes(img.mode, (width, height), image_data, "raw", "RGBA")
356+
357+
if original_width != width or original_height != height:
358+
img = img.crop((0, 0, original_width, original_height))
359+
210360
if img and flip:
211361
return img.transpose(Image.FLIP_TOP_BOTTOM)
212362

@@ -229,7 +379,7 @@ def pillow(
229379
mode: str,
230380
codec: str,
231381
args,
232-
swap: Optional[tuple] = None,
382+
swap: Optional[Tuple[int, ...]] = None,
233383
) -> Image.Image:
234384
img = (
235385
Image.frombytes(mode, (width, height), image_data, codec, args)

0 commit comments

Comments
 (0)