1919
2020TF = 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
23137def 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
127259def 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