├── .gitattributes ├── Main.py ├── Writer.py ├── dumpsc_example.PNG ├── readme.md └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import lzma 4 | import lzham 5 | import hashlib 6 | import argparse 7 | import zstandard 8 | 9 | from PIL import Image 10 | from Writer import BinaryWriter 11 | 12 | 13 | class Packer(BinaryWriter): 14 | 15 | def __init__(self, use_lzma, use_lzham, use_zstd, splitting, header, outputName): 16 | self.settings = { 17 | 'use_lzma': use_lzma, 18 | 'use_lzham': use_lzham, 19 | 'use_zstd': use_zstd, 20 | 'splitting': splitting, 21 | 'header': header, 22 | 'outputname': outputName 23 | } 24 | self.image_list = [] 25 | super().__init__() 26 | 27 | def load_image(self, path, pixelFormat): 28 | if pixelFormat in (0, 1, 2, 3, 4, 6, 10): 29 | self.image_list.append({ 30 | "Image": Image.open(path), 31 | "Path": path, 32 | "PixelFormat": pixelFormat 33 | }) 34 | 35 | else: 36 | print('[*] Unsupported pixelformat ({}) ! '.format(pixelFormat)) 37 | sys.exit() 38 | 39 | def pack(self): 40 | pixelSizeList = { 41 | 0: 4, 42 | 1: 4, 43 | 2: 2, 44 | 3: 2, 45 | 4: 2, 46 | 6: 2, 47 | 10: 1 48 | } 49 | 50 | if self.settings['splitting']: 51 | fileType = 28 52 | 53 | else: 54 | fileType = 1 55 | 56 | for image in self.image_list: 57 | pixelFormat = image['PixelFormat'] 58 | pixelSize = pixelSizeList[pixelFormat] 59 | imageWidth = image['Image'].width 60 | imageHeight = image['Image'].height 61 | texureSize = imageWidth * imageHeight * pixelSize + 5 62 | 63 | print('[INFO] Packing {}, width: {}, height: {}, pixelformat: {}'.format(image['Path'], imageHeight, imageWidth, pixelFormat)) 64 | 65 | # Texture header 66 | self.write_uint8(fileType) 67 | self.write_uint32(texureSize) 68 | self.write_uint8(pixelFormat) 69 | self.write_uint16(imageWidth) 70 | self.write_uint16(imageHeight) 71 | 72 | if self.settings['splitting']: 73 | self.split_image(image['Image']) 74 | 75 | pixels = image['Image'].load() 76 | 77 | for y in range(imageHeight): 78 | for x in range(imageWidth): 79 | colors = pixels[x, y] 80 | self.write_pixel(pixelFormat, colors) 81 | 82 | self.write(5) 83 | 84 | if True in (self.settings['use_lzma'], self.settings['use_lzham'], self.settings['use_zstd']): 85 | self.compress_data() 86 | 87 | if self.settings['outputname']: 88 | outputName = self.settings['outputname'] 89 | 90 | else: 91 | outputName = os.path.splitext(self.image_list[0]['Path'])[0].rstrip('_') + '.sc' 92 | 93 | with open(outputName, 'wb') as f: 94 | f.write(self.buffer) 95 | 96 | def split_image(self, image): 97 | imageWidth = image.width 98 | imageHeight = image.height 99 | imgl = image.load() 100 | pixels = [] 101 | 102 | print('[*] Splitting texture') 103 | 104 | for l in range(imageHeight // 32): 105 | for k in range(imageWidth // 32): 106 | for j in range(32): 107 | for h in range(32): 108 | pixels.append(imgl[h + (k * 32), j + (l * 32)]) 109 | 110 | for j in range(32): 111 | for h in range(imageWidth % 32): 112 | pixels.append(imgl[h + (imageWidth - (imageWidth % 32)), j + (l * 32)]) 113 | 114 | for k in range(imageWidth // 32): 115 | for j in range(int(imageHeight % 32)): 116 | for h in range(32): 117 | pixels.append(imgl[h + (k * 32), j + (imageHeight - (imageHeight % 32))]) 118 | 119 | for j in range(imageHeight % 32): 120 | for h in range(imageWidth % 32): 121 | pixels.append(imgl[h + (imageWidth - (imageWidth % 32)), j + (imageHeight - (imageHeight % 32))]) 122 | 123 | image.putdata(pixels) 124 | print('[*] Splitting done !') 125 | 126 | def write_pixel(self, pixelFormat, colors): 127 | red, green, blue, alpha = colors 128 | 129 | if pixelFormat in (0, 1): 130 | # RGBA8888 131 | self.write_uint8(red) 132 | self.write_uint8(green) 133 | self.write_uint8(blue) 134 | self.write_uint8(alpha) 135 | 136 | elif pixelFormat == 2: 137 | # RGBA8888 to RGBA4444 138 | r = (red >> 4) << 12 139 | g = (green >> 4) << 8 140 | b = (blue >> 4) << 4 141 | a = alpha >> 4 142 | 143 | self.write_uint16(a | b | g | r) 144 | 145 | elif pixelFormat == 3: 146 | # RGBA8888 to RGBA5551 147 | r = (red >> 3) << 11 148 | g = (green >> 3) << 6 149 | b = (blue >> 3) << 1 150 | a = alpha >> 7 151 | 152 | self.write_uint16(a | b | g | r) 153 | 154 | elif pixelFormat == 4: 155 | # RGBA8888 to RGBA565 156 | r = (red >> 3) << 11 157 | g = (green >> 2) << 5 158 | b = blue >> 3 159 | 160 | self.write_uint16(b | g | r) 161 | 162 | elif pixelFormat == 6: 163 | # RGBA8888 to LA88 (Luminance Alpha) 164 | self.write_uint8(alpha) 165 | self.write_uint8(red) 166 | 167 | elif pixelFormat == 10: 168 | # RGBA8888 to L8 (Luminance) 169 | self.write_uint8(red) 170 | 171 | def compress_data(self): 172 | if self.settings['use_lzma']: 173 | print('[*] Compressing texture with lzma') 174 | 175 | filters = [ 176 | { 177 | "id": lzma.FILTER_LZMA1, 178 | "dict_size": 256 * 1024, 179 | "lc": 3, 180 | "lp": 0, 181 | "pb": 2, 182 | "mode": lzma.MODE_NORMAL 183 | }, 184 | ] 185 | 186 | compressed = lzma.compress(self.buffer, format=lzma.FORMAT_ALONE, filters=filters) 187 | compressed = compressed[0:5] + len(self.buffer).to_bytes(4, 'little') + compressed[13:] 188 | 189 | elif self.settings['use_lzham']: 190 | print('[*] Compressing texture with lzham') 191 | 192 | dict_size = 18 193 | 194 | compressed = lzham.compress(self.buffer, {'dict_size_log2': dict_size}) 195 | compressed = 'SCLZ'.encode('utf-8') + dict_size.to_bytes(1, 'big') + len(self.buffer).to_bytes(4, 'little') + compressed 196 | 197 | else: 198 | print('[*] Compressing texture with zstandard') 199 | compressed = zstandard.compress(self.buffer, level=zstandard.MAX_COMPRESSION_LEVEL) 200 | 201 | fileMD5 = hashlib.md5(self.buffer).digest() 202 | 203 | # Flush the previous buffer 204 | self.buffer = b'' 205 | 206 | if self.settings['header']: 207 | self.write('SC'.encode('utf-8')) 208 | 209 | if self.settings['use_zstd']: 210 | self.write_uint32(3, 'big') 211 | 212 | else: 213 | self.write_uint32(1, 'big') 214 | 215 | self.write_uint32(len(fileMD5), 'big') 216 | self.write(fileMD5) 217 | 218 | print('[*] Header wrote !') 219 | 220 | self.write(compressed) 221 | 222 | print('[*] Compression done !') 223 | 224 | 225 | if __name__ == "__main__": 226 | parser = argparse.ArgumentParser(description="scPacker is a tool that allows you to convert PNG files to _tex.sc files") 227 | parser.add_argument('files', help='.png file(s) to pack', nargs='+') 228 | parser.add_argument('-lzma', '--lzma', help='enable LZMA compression', action='store_true') 229 | parser.add_argument('-lzham', '--lzham', help='enable LZHAM compression', action='store_true') 230 | parser.add_argument('-zstd', '--zstd', help='enable Zstandard compression', action='store_true') 231 | parser.add_argument('-header', '--header', help='add SC header to the beginning of the compressed _tex.sc', action='store_true') 232 | parser.add_argument('-o', '--outputname', help='define an output name for the _tex.sc file (if not specified the output filename is set to + _tex.sc') 233 | parser.add_argument('-p', '--pixelformat', help='pixelformat(s) to be used to pack .png to _tex.sc', nargs='+', type=int) 234 | parser.add_argument('-s', '--splitting', help='enable 32x32 block splitting', action='store_true') 235 | 236 | args = parser.parse_args() 237 | 238 | if args.pixelformat: 239 | if len(args.files) == len(args.pixelformat): 240 | if (args.lzham, args.lzma, args.zstd).count(True) == 1: 241 | scPacker = Packer(args.lzma, args.lzham, args.zstd, args.splitting, args.header, args.outputname) 242 | 243 | for file, pixelFormat in zip(args.files, args.pixelformat): 244 | if file.endswith('.png'): 245 | if os.path.exists(file): 246 | scPacker.load_image(file, pixelFormat) 247 | 248 | else: 249 | print('[*] {} doesn\'t exists !'.format(file)) 250 | sys.exit() 251 | 252 | else: 253 | print('[*] Only .png are supported !') 254 | sys.exit() 255 | 256 | scPacker.pack() 257 | 258 | else: 259 | print('[*] You cannot set many compression at a time !') 260 | 261 | else: 262 | print('[*] Files count and pixelformats count don\'t match !') 263 | 264 | else: 265 | print('[*] PixelFormat args is empty !') 266 | -------------------------------------------------------------------------------- /Writer.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | 4 | class BinaryWriter: 5 | 6 | def __init__(self): 7 | self._buffer = BytesIO() 8 | 9 | @property 10 | def buffer(self): 11 | return self._buffer.getvalue() 12 | 13 | @buffer.setter 14 | def buffer(self, data): 15 | self._buffer.seek(0) 16 | self._buffer.truncate(0) 17 | self._buffer.write(data) 18 | 19 | def write(self, value): 20 | self._buffer.write(bytes(value)) 21 | 22 | def write_uint8(self, value): 23 | self._buffer.write(value.to_bytes(1, 'little')) 24 | 25 | def write_int8(self, value): 26 | self._buffer.write(value.to_bytes(1, 'little', signed=True)) 27 | 28 | def write_uint16(self, value): 29 | self._buffer.write(value.to_bytes(2, 'little')) 30 | 31 | def write_int16(self, value): 32 | self._buffer.write(value.to_bytes(2, 'little', signed=True)) 33 | 34 | def write_uint32(self, value, byteorder='little'): 35 | self._buffer.write(value.to_bytes(4, byteorder)) 36 | 37 | def write_int32(self, value): 38 | self._buffer.write(value.to_bytes(4, 'little')) 39 | -------------------------------------------------------------------------------- /dumpsc_example.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxy1036/scPacker/71e2dae4351c73c85041bf177f4bff9b9855d5fb/dumpsc_example.PNG -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## scPacker 2 | **scPacker** is a python script that allows you to convert 3 | PNG files to _tex.sc files, **\_tex.sc** files are specific files used by **Supercell** in their own game engine. 4 | 5 | ### How to use it ? 6 | The basic usage to pack one file is: 7 | 8 | > python Main.py -p 9 | 10 | Example: 11 | 12 | > python Main.py ui_tex.png -p 0 13 | 14 | ---------- 15 | 16 | However if you want to pack multiple files use: 17 | 18 | > python Main.py ... -p ... 19 | 20 | Example: 21 | 22 | > python Main.py ui\_tex.png ui\_tex\_.png -p 0 4 23 | 24 | in this case ui\_tex.png will be packed using pixelformat 0 and ui\_tex\_.png using pixelformat 4. 25 | 26 | **Warning**: You should set as many pixelformats as filenames you've set. 27 | 28 | ### Options 29 | **scPacker** can also takes few optionals arguments which are: 30 | 31 | * `-lzma`: if this argument is specified tex.sc file will be compressed using lzma 32 | * `-lzham`: if this argument is specified tex.sc file will be compressed using lzham 33 | * `-zstd`: if this argument is specified tex.sc file will be compressed using zstandard 34 | * `-header`: add Supercell header at the beginning of the compressed tex.sc file 35 | * `-o`: optionnal output filename for the tex.sc file, if this argument isn't specified tex.sc file will be saved as + _tex.sc 36 | * `-s`: enable 32x32 block texture splitting, 32x32 block splitting is used in most of the original Supercell _tex.sc files 37 | 38 | Command Example: 39 | > python Main.py loading\_tex.png loading\_tex\_.png loading\_tex\_\_.png -p 0 4 6 -lzma -header -s -o afilename\_tex.sc 40 | 41 | ### How do i know which pixeltype to use if i want to pack an original texture ? 42 | Let's take an example from an original \_tex.sc files. For this example we'll use loading\_tex.sc from Clash Royale. 43 | First we'll extract the .png textures using [Dumpsc](https://github.com/Galaxy1036/Dumpsc). 44 | Here is the console logs after extracting textures: 45 | 46 | ![Image](/dumpsc_example.PNG) 47 | 48 | According to the logs it seems that loading\_tex.png use pixelformat 0, loading\_tex\_.png use pixelformat 4 and loading\_tex\_\_.png use pixelformat 6. 49 | 50 | Basically to re-pack these .png we'll use the following command: 51 | > python Main.py loading_tex.png loading\_tex\_.png loading\_tex\_\_.png -p 0 4 6 52 | 53 | ### Dependencies 54 | To install **scPacker** dependencies run the following command 55 | 56 | > python -m pip install -r requirements.txt 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pylzham 2 | Pillow 3 | zstandard --------------------------------------------------------------------------------