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+ 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
23130def 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
127252def 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