Skip to content

Commit 7427288

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

2 files changed

Lines changed: 216 additions & 70 deletions

File tree

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: 212 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -19,80 +19,177 @@
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+
width = (width + block_width - 1) & ~(block_width - 1)
42+
height = (height + block_height - 1) & ~(block_height - 1)
43+
return width, height
44+
45+
46+
def pad_image(img: Image.Image, pad_width: int, pad_height: int) -> Image.Image:
47+
ori_width, ori_height = img.size
48+
if pad_width == ori_width and pad_height == ori_height:
49+
return img
50+
51+
pad_img = Image.new(img.mode, (pad_width, pad_height))
52+
pad_img.paste(img)
53+
54+
# Paste the original image at the top-left corner
55+
pad_img.paste(img, (0, 0))
56+
57+
# Fill the right border: duplicate the last column
58+
if pad_width != ori_width:
59+
right_strip = img.crop((ori_width - 1, 0, ori_width, ori_height))
60+
right_strip = right_strip.resize(
61+
(pad_width - ori_width, ori_height), resample=Image.NEAREST
62+
)
63+
pad_img.paste(right_strip, (ori_width, 0))
64+
65+
# Fill the bottom border: duplicate the last row
66+
if pad_height != ori_height:
67+
bottom_strip = img.crop((0, ori_height - 1, ori_width, ori_height))
68+
bottom_strip = bottom_strip.resize(
69+
(ori_width, pad_height - ori_height), resample=Image.NEAREST
70+
)
71+
pad_img.paste(bottom_strip, (0, ori_height))
72+
73+
# Fill the bottom-right corner with the bottom-right pixel
74+
if pad_width != ori_width and pad_height != ori_height:
75+
corner = img.getpixel((ori_width - 1, ori_height - 1))
76+
corner_img = Image.new(
77+
img.mode, (pad_width - ori_width, pad_height - ori_height), color=corner
78+
)
79+
pad_img.paste(corner_img, (ori_width, ori_height))
80+
81+
return pad_img
82+
83+
84+
def compress_etcpak(
85+
data: bytes, width: int, height: int, target_texture_format: TextureFormat
86+
) -> bytes:
87+
import etcpak
88+
89+
if target_texture_format in [TF.DXT1, TF.DXT1Crunched]:
90+
return etcpak.compress_bc1(data, width, height)
91+
elif target_texture_format in [TF.DXT5, TF.DXT5Crunched]:
92+
return etcpak.compress_bc3(data, width, height)
93+
elif target_texture_format == TF.BC4:
94+
return etcpak.compress_bc4(data, width, height)
95+
elif target_texture_format == TF.BC5:
96+
return etcpak.compress_bc5(data, width, height)
97+
elif target_texture_format == TF.BC7:
98+
return etcpak.compress_bc7(data, width, height, None)
99+
elif target_texture_format in [TF.ETC_RGB4, TF.ETC_RGB4Crunched, TF.ETC_RGB4_3DS]:
100+
return etcpak.compress_etc1_rgb(data, width, height)
101+
elif target_texture_format == TF.ETC2_RGB:
102+
return etcpak.compress_etc2_rgb(data, width, height)
103+
elif target_texture_format in [TF.ETC2_RGBA8, TF.ETC2_RGBA8Crunched, TF.ETC2_RGBA1]:
104+
return etcpak.compress_etc2_rgba(data, width, height)
105+
else:
106+
raise NotImplementedError(
107+
f"etcpak has no compress function for {target_texture_format.name}"
108+
)
109+
110+
111+
def compress_astc(
112+
data: bytes, width: int, height: int, target_texture_format: TextureFormat
113+
) -> bytes:
114+
astc_image = astc_encoder.ASTCImage(
115+
astc_encoder.ASTCType.U8, width, height, 1, data
116+
)
117+
block_size = TEXTURE_FORMAT_BLOCK_SIZE_TABLE[target_texture_format]
118+
assert block_size is not None, (
119+
f"failed to get block size for {target_texture_format.name}"
120+
)
121+
swizzle = astc_encoder.ASTCSwizzle.from_str("RGBA")
122+
123+
context, lock = get_astc_context(block_size)
124+
with lock:
125+
enc_img = context.compress(astc_image, swizzle)
126+
127+
return enc_img
128+
22129

23130
def image_to_texture2d(
24-
img: Image.Image, target_texture_format: Union[TF, int], flip: bool = True
131+
img: Image.Image,
132+
target_texture_format: Union[TF, int],
133+
platform: int = 0,
134+
platform_blob: Optional[bytes] = None,
135+
flip: bool = True,
25136
) -> Tuple[bytes, TextureFormat]:
26-
if isinstance(target_texture_format, int):
137+
if not isinstance(target_texture_format, TextureFormat):
27138
target_texture_format = TextureFormat(target_texture_format)
28139

29-
import etcpak
30-
31140
if flip:
32141
img = img.transpose(Image.FLIP_TOP_BOTTOM)
33142

143+
# defaults
144+
compress_func = None
145+
tex_format = TF.RGBA32
146+
pil_mode = "RGBA"
147+
34148
# DXT
35149
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)
38150
tex_format = TF.DXT1
151+
compress_func = compress_etcpak
39152
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)
42153
tex_format = TF.DXT5
154+
compress_func = compress_etcpak
43155
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)
46156
tex_format = TF.BC4
157+
compress_func = compress_etcpak
47158
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)
50159
tex_format = TF.BC5
160+
compress_func = compress_etcpak
51161
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)
54162
tex_format = TF.BC7
163+
compress_func = compress_etcpak
164+
# ASTC
165+
elif target_texture_format.name.startswith("ASTC"):
166+
if "_HDR_" in target_texture_format.name:
167+
block_size = TEXTURE_FORMAT_BLOCK_SIZE_TABLE[target_texture_format]
168+
assert block_size is not None
169+
if img.mode == "RGB":
170+
tex_format = getattr(TF, f"ASTC_RGB_{block_size[0]}x{block_size[1]}")
171+
else:
172+
tex_format = getattr(TF, f"ASTC_RGBA_{block_size[0]}x{block_size[1]}")
173+
else:
174+
tex_format = target_texture_format
175+
compress_func = compress_astc
55176
# ETC
56177
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
178+
if target_texture_format == TF.ETC_RGB4_3DS:
179+
tex_format = TF.ETC_RGB4_3DS
180+
else:
181+
tex_format = target_texture_format
182+
compress_func = compress_etcpak
60183
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)
63184
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)
185+
compress_func = compress_etcpak
186+
elif target_texture_format in [TF.ETC2_RGBA8, TF.ETC2_RGBA8Crunched, TF.ETC2_RGBA1]:
70187
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
188+
compress_func = compress_etcpak
92189
# A
93190
elif target_texture_format == TF.Alpha8:
94-
enc_img = img.tobytes("raw", "A")
95191
tex_format = TF.Alpha8
192+
pil_mode = "A"
96193
# R - should probably be moerged into #A, as pure R is used as Alpha
97194
# but need test data for this first
98195
elif target_texture_format in [
@@ -103,33 +200,61 @@ def image_to_texture2d(
103200
TF.EAC_R,
104201
TF.EAC_R_SIGNED,
105202
]:
106-
enc_img = img.tobytes("raw", "R")
107203
tex_format = TF.R8
204+
pil_mode = "R"
108205
# RGBA
109206
elif target_texture_format in [
110207
TF.RGB565,
111208
TF.RGB24,
209+
TF.BGR24,
112210
TF.RGB9e5Float,
113211
TF.PVRTC_RGB2,
114212
TF.PVRTC_RGB4,
115213
TF.ATC_RGB4,
116214
]:
117-
enc_img = img.tobytes("raw", "RGB")
118215
tex_format = TF.RGB24
216+
pil_mode = "RGB"
119217
# everything else defaulted to RGBA
218+
219+
if platform == BuildTarget.Switch and platform_blob is not None:
220+
gobsPerBlock = TextureSwizzler.get_switch_gobs_per_block(platform_blob)
221+
s_tex_format = tex_format
222+
if tex_format == TextureFormat.RGB24:
223+
s_tex_format = TextureFormat.RGBA32
224+
pil_mode = "RGBA"
225+
# elif tex_format == TextureFormat.BGR24:
226+
# s_tex_format = TextureFormat.BGRA32
227+
block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[s_tex_format]
228+
width, height = TextureSwizzler.get_padded_texture_size(
229+
img.width, img.height, *block_size, gobsPerBlock
230+
)
231+
img = pad_image(img, width, height)
232+
img = Image.frombytes(
233+
"RGBA",
234+
img.size,
235+
TextureSwizzler.swizzle(
236+
img.tobytes("raw", "RGBA"), width, height, *block_size, gobsPerBlock
237+
),
238+
)
239+
240+
if compress_func:
241+
width, height = get_compressed_image_size(img.width, img.height, tex_format)
242+
img = pad_image(img, width, height)
243+
enc_img = compress_func(
244+
img.tobytes("raw", "RGBA"), img.width, img.height, tex_format
245+
)
120246
else:
121-
enc_img = img.tobytes("raw", "RGBA")
122-
tex_format = TF.RGBA32
247+
enc_img = img.tobytes("raw", pil_mode)
123248

124249
return enc_img, tex_format
125250

126251

127252
def assert_rgba(img: Image.Image, target_texture_format: TextureFormat) -> Image.Image:
128253
if img.mode == "RGB":
129254
img = img.convert("RGBA")
130-
assert (
131-
img.mode == "RGBA"
132-
), f"{target_texture_format} compression only supports RGB & RGBA images" # noqa: E501
255+
assert img.mode == "RGBA", (
256+
f"{target_texture_format} compression only supports RGB & RGBA images"
257+
) # noqa: E501
133258
return img
134259

135260

@@ -163,36 +288,45 @@ def parse_image_data(
163288
width: int,
164289
height: int,
165290
texture_format: Union[int, TextureFormat],
166-
version: tuple,
291+
version: Tuple[int, int, int, int],
167292
platform: int,
168293
platform_blob: Optional[bytes] = None,
169294
flip: bool = True,
170295
) -> Image.Image:
296+
if not width or not height:
297+
return Image.new("RGBA", (0, 0))
298+
171299
image_data = copy(bytes(image_data))
172300
if not image_data:
173301
raise ValueError("Texture2D has no image data")
174302

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

182306
if platform == BuildTarget.XBOX360 and texture_format in XBOX_SWAP_FORMATS:
183307
image_data = swap_bytes_for_xbox(image_data)
184-
elif platform == BuildTarget.Switch and platform_blob is not None:
308+
309+
original_width, original_height = (width, height)
310+
switch_swizzle = None
311+
if platform == BuildTarget.Switch and platform_blob is not None:
185312
gobsPerBlock = TextureSwizzler.get_switch_gobs_per_block(platform_blob)
313+
if texture_format == TextureFormat.RGB24:
314+
texture_format = TextureFormat.RGBA32
315+
elif texture_format == TextureFormat.BGR24:
316+
texture_format = TextureFormat.BGRA32
186317
block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[texture_format]
187-
padded_size = TextureSwizzler.get_padded_texture_size(
318+
width, height = TextureSwizzler.get_padded_texture_size(
188319
width, height, *block_size, gobsPerBlock
189320
)
190-
image_data = TextureSwizzler.deswizzle(
191-
image_data, *padded_size, *block_size, gobsPerBlock
192-
)
321+
switch_swizzle = (block_size, gobsPerBlock)
322+
else:
323+
width, height = get_compressed_image_size(width, height, texture_format)
324+
325+
selection = CONV_TABLE[texture_format]
326+
327+
if len(selection) == 0:
328+
raise NotImplementedError(f"Not implemented texture format: {texture_format}")
193329

194-
if not isinstance(texture_format, TextureFormat):
195-
texture_format = TextureFormat(texture_format)
196330
if "Crunched" in texture_format.name:
197331
version = version
198332
if (
@@ -207,6 +341,15 @@ def parse_image_data(
207341

208342
img = selection[0](image_data, width, height, *selection[1:])
209343

344+
if switch_swizzle is not None:
345+
image_data = TextureSwizzler.deswizzle(
346+
img.tobytes("raw", "RGBA"), width, height, *block_size, gobsPerBlock
347+
)
348+
img = Image.frombytes(img.mode, (width, height), image_data, "raw", "RGBA")
349+
350+
if original_width != width or original_height != height:
351+
img = img.crop((0, 0, original_width, original_height))
352+
210353
if img and flip:
211354
return img.transpose(Image.FLIP_TOP_BOTTOM)
212355

@@ -229,7 +372,7 @@ def pillow(
229372
mode: str,
230373
codec: str,
231374
args,
232-
swap: Optional[tuple] = None,
375+
swap: Optional[Tuple[int, ...]] = None,
233376
) -> Image.Image:
234377
img = (
235378
Image.frombytes(mode, (width, height), image_data, codec, args)

0 commit comments

Comments
 (0)