用python脚本来拆分spine的atlas图集 作者: ciniao 时间: 2025-01-06 分类: 技术 spine输出的文件里,包含了png和atlas文件,其中atlas文件记录了png散图的信息。 碰到一个需求,需要将png+atlas还原为对应的散图,网上找了一些脚本,都不完整,要么是不支持offset,要么是不支持rotate,遂自己写了一个 环境:`python3.8 + spine3.8.9` spine atlas的文件内容示意如下 ``` 15001.png size: 1024,1024 format: RGBA8888 filter: Linear,Linear repeat: none a1 rotate: false xy: 2, 672 size: 148, 77 orig: 148, 77 offset: 0, 0 index: -1 a2 rotate: false xy: 456, 955 size: 155, 35 orig: 155, 35 offset: 0, 0 index: -1 a3 rotate: false xy: 474, 992 size: 270, 30 orig: 270, 30 offset: 0, 0 index: -1 a4 rotate: false xy: 196, 536 size: 314, 99 orig: 314, 99 offset: 0, 0 index: -1 a5 rotate: false xy: 719, 11 size: 229, 137 orig: 229, 137 offset: 0, 0 index: -1 b1 rotate: false xy: 2, 817 size: 115, 56 orig: 115, 56 offset: 0, 0 index: -1 b2 rotate: false xy: 209, 995 size: 132, 27 orig: 132, 27 offset: 0, 0 index: -1 b3 rotate: false xy: 119, 813 size: 278, 52 orig: 278, 52 offset: 0, 0 index: -1 b4 rotate: false xy: 298, 637 size: 339, 81 orig: 339, 81 offset: 0, 0 index: -1 b5 rotate: false xy: 327, 271 size: 251, 116 orig: 254, 117 offset: 3, 1 index: -1 body rotate: false xy: 751, 597 size: 269, 100 orig: 269, 100 offset: 0, 0 index: -1 foot_A rotate: false xy: 453, 912 size: 62, 41 orig: 62, 41 offset: 0, 0 index: -1 foot_B rotate: false xy: 954, 888 size: 64, 46 orig: 64, 46 offset: 0, 0 index: -1 foot_C rotate: false xy: 734, 905 size: 62, 44 orig: 66, 44 offset: 4, 0 index: -1 foot_D rotate: false xy: 124, 871 size: 67, 46 orig: 67, 46 offset: 0, 0 index: -1 jiao rotate: false xy: 901, 898 size: 51, 46 orig: 53, 46 offset: 2, 0 index: -1 leg_A1 rotate: true xy: 711, 952 size: 38, 62 orig: 40, 62 offset: 0, 0 index: -1 leg_A2 rotate: false xy: 881, 989 size: 43, 33 orig: 46, 33 offset: 0, 0 index: -1 leg_A3 rotate: false xy: 983, 978 size: 38, 44 orig: 38, 44 offset: 0, 0 index: -1 leg_B1 rotate: true xy: 49, 962 size: 35, 48 orig: 35, 48 offset: 0, 0 index: -1 leg_B2 rotate: false xy: 2, 926 size: 43, 41 orig: 43, 44 offset: 0, 0 index: -1 leg_B3 rotate: true xy: 157, 996 size: 26, 50 orig: 26, 50 offset: 0, 0 index: -1 leg_C1 rotate: true xy: 328, 867 size: 47, 70 orig: 47, 70 offset: 0, 0 index: -1 leg_C2 rotate: false xy: 775, 951 size: 44, 38 orig: 44, 38 offset: 0, 0 index: -1 leg_C3 rotate: true xy: 926, 988 size: 34, 55 orig: 34, 55 offset: 0, 0 index: -1 leg_D1 rotate: true xy: 193, 871 size: 46, 79 orig: 46, 79 offset: 0, 0 index: -1 leg_D2 rotate: false xy: 798, 904 size: 50, 44 orig: 50, 44 offset: 0, 0 index: -1 leg_D3 rotate: true xy: 343, 994 size: 28, 57 orig: 28, 57 offset: 0, 0 index: -1 tou rotate: false xy: 639, 612 size: 110, 100 orig: 110, 100 offset: 0, 0 index: -1 tx/guang rotate: false xy: 631, 909 size: 43, 43 orig: 51, 51 offset: 4, 4 index: -1 tx/hou/shengbo_00000 rotate: true xy: 22, 1009 size: 13, 15 orig: 128, 128 offset: 51, 62 index: -1 tx/hou/shengbo_00002 rotate: true xy: 86, 999 size: 23, 28 orig: 128, 128 offset: 39, 50 index: -1 tx/hou/shengbo_00004 rotate: false xy: 918, 946 size: 42, 40 orig: 128, 128 offset: 24, 37 index: -1 tx/hou/shengbo_00006 rotate: true xy: 633, 851 size: 54, 56 orig: 128, 128 offset: 13, 24 index: -1 tx/hou/shengbo_00008 rotate: true xy: 646, 788 size: 59, 63 orig: 128, 128 offset: 8, 19 index: -1 tx/hou/shengbo_00010 rotate: false xy: 335, 747 size: 62, 64 orig: 128, 128 offset: 5, 19 index: -1 tx/hou/shengbo_00012 rotate: true xy: 711, 787 size: 59, 64 orig: 128, 128 offset: 7, 19 index: -1 tx/hou/shengbo_00014 rotate: true xy: 927, 829 size: 57, 58 orig: 128, 128 offset: 9, 22 index: -1 tx/hou/shengbo_00016 rotate: true xy: 399, 803 size: 57, 60 orig: 128, 128 offset: 8, 21 index: -1 tx/hou/shengbo_00018 rotate: false xy: 840, 778 size: 60, 60 orig: 128, 128 offset: 5, 19 index: -1 tx/hou/shengbo_00020 rotate: true xy: 461, 800 size: 57, 61 orig: 128, 128 offset: 8, 19 index: -1 tx/hou/shengbo_00022 rotate: true xy: 524, 795 size: 57, 58 orig: 128, 128 offset: 9, 22 index: -1 tx/hou/shengbo_00024 rotate: true xy: 777, 782 size: 59, 61 orig: 128, 128 offset: 5, 20 index: -1 tx/hou/shengbo_00026 rotate: false xy: 2, 755 size: 60, 60 orig: 128, 128 offset: 5, 19 index: -1 tx/hou/shengbo_00028 rotate: true xy: 807, 843 size: 56, 59 orig: 128, 128 offset: 8, 20 index: -1 tx/hou/shengbo_00030 rotate: true xy: 868, 840 size: 56, 57 orig: 128, 128 offset: 8, 21 index: -1 tx/hou/shengbo_00032 rotate: false xy: 64, 751 size: 61, 60 orig: 128, 128 offset: 5, 19 index: -1 tx/hou/shengbo_00034 rotate: false xy: 272, 750 size: 61, 61 orig: 128, 128 offset: 5, 19 index: -1 tx/hou/shengbo_00036 rotate: true xy: 691, 849 size: 54, 57 orig: 128, 128 offset: 11, 24 index: -1 tx/hou/shengbo_00038 rotate: true xy: 584, 792 size: 57, 60 orig: 128, 128 offset: 8, 21 index: -1 tx/skill/1/yan_00 rotate: false xy: 465, 731 size: 96, 62 orig: 120, 80 offset: 12, 8 index: -1 tx/skill/1/yan_02 rotate: false xy: 831, 699 size: 95, 67 orig: 120, 80 offset: 13, 6 index: -1 tx/skill/1/yan_04 rotate: false xy: 635, 720 size: 96, 65 orig: 120, 80 offset: 12, 5 index: -1 tx/skill/1/yan_06 rotate: false xy: 928, 699 size: 94, 67 orig: 120, 80 offset: 12, 6 index: -1 tx/skill/1/yan_08 rotate: false xy: 733, 714 size: 96, 66 orig: 120, 80 offset: 12, 6 index: -1 tx/skill/10/koupenhuo_00 rotate: true xy: 664, 954 size: 36, 45 orig: 80, 80 offset: 24, 17 index: -1 tx/skill/10/koupenhuo_01 rotate: true xy: 821, 950 size: 38, 47 orig: 80, 80 offset: 23, 17 index: -1 tx/skill/10/koupenhuo_02 rotate: true xy: 870, 948 size: 39, 46 orig: 80, 80 offset: 23, 18 index: -1 tx/skill/10/koupenhuo_03 rotate: true xy: 781, 991 size: 31, 49 orig: 80, 80 offset: 25, 17 index: -1 tx/skill/10/koupenhuo_04 rotate: true xy: 832, 990 size: 32, 47 orig: 80, 80 offset: 24, 18 index: -1 tx/skill/3/penhuo_00 rotate: true xy: 170, 301 size: 121, 155 orig: 160, 200 offset: 18, 32 index: -1 tx/skill/3/penhuo_02 rotate: true xy: 2, 317 size: 117, 166 orig: 160, 200 offset: 18, 22 index: -1 tx/skill/3/penhuo_04 rotate: true xy: 169, 145 size: 124, 170 orig: 160, 200 offset: 20, 18 index: -1 tx/skill/3/penhuo_06 rotate: true xy: 798, 275 size: 122, 163 orig: 160, 200 offset: 19, 24 index: -1 tx/skill/3/penhuo_08 rotate: true xy: 745, 150 size: 123, 168 orig: 160, 200 offset: 20, 19 index: -1 tx/skill/3/penhuo_10 rotate: true xy: 2, 191 size: 124, 165 orig: 160, 200 offset: 16, 22 index: -1 tx/skill/3/penhuo_12 rotate: true xy: 580, 249 size: 122, 163 orig: 160, 200 offset: 19, 24 index: -1 tx/skill/3/penhuo_14 rotate: true xy: 2, 57 size: 132, 161 orig: 160, 200 offset: 14, 27 index: -1 tx/skill/4/penhuoxiaoshi_00 rotate: true xy: 547, 119 size: 128, 170 orig: 160, 200 offset: 14, 27 index: -1 tx/skill/4/penhuoxiaoshi_02 rotate: true xy: 152, 659 size: 89, 144 orig: 160, 200 offset: 35, 42 index: -1 tx/skill/5/quan_00 rotate: false xy: 2, 875 size: 120, 43 orig: 120, 60 offset: 0, 15 index: -1 tx/skill/5/quan_01 rotate: false xy: 287, 916 size: 120, 38 orig: 120, 60 offset: 0, 16 index: -1 tx/skill/5/quan_02 rotate: false xy: 47, 920 size: 118, 37 orig: 120, 60 offset: 1, 16 index: -1 tx/skill/5/quan_03 rotate: false xy: 167, 919 size: 118, 37 orig: 120, 60 offset: 1, 16 index: -1 tx/skill/5/quan_04 rotate: false xy: 337, 956 size: 117, 36 orig: 120, 60 offset: 2, 17 index: -1 tx/skill/5/quan_05 rotate: false xy: 218, 958 size: 117, 35 orig: 120, 60 offset: 2, 17 index: -1 tx/skill/5/quan_06 rotate: false xy: 99, 959 size: 117, 34 orig: 120, 60 offset: 2, 17 index: -1 tx/skill/6/huo_01 rotate: true xy: 576, 854 size: 53, 55 orig: 60, 60 offset: 6, 5 index: -1 tx/skill/6/huo_03 rotate: false xy: 750, 848 size: 55, 54 orig: 60, 60 offset: 1, 5 index: -1 tx/skill/6/huo_05 rotate: false xy: 461, 861 size: 55, 49 orig: 60, 60 offset: 1, 5 index: -1 tx/skill/6/huo_07 rotate: false xy: 850, 901 size: 49, 45 orig: 60, 60 offset: 3, 5 index: -1 tx/skill/6/huo_09 rotate: false xy: 676, 907 size: 56, 43 orig: 60, 60 offset: 2, 5 index: -1 tx/skill/6/huo_11 rotate: false xy: 274, 868 size: 52, 46 orig: 60, 60 offset: 2, 5 index: -1 tx/skill/6/huo_13 rotate: false xy: 518, 859 size: 56, 49 orig: 60, 60 offset: 4, 5 index: -1 tx/skill/7/huo2_00 rotate: false xy: 713, 485 size: 191, 110 orig: 240, 160 offset: 28, 26 index: -1 tx/skill/7/huo2_02 rotate: false xy: 2, 548 size: 192, 109 orig: 240, 160 offset: 24, 26 index: -1 tx/skill/7/huo2_04 rotate: false xy: 195, 424 size: 203, 110 orig: 240, 160 offset: 14, 26 index: -1 tx/skill/7/huo2_06 rotate: false xy: 400, 389 size: 197, 110 orig: 240, 160 offset: 20, 26 index: -1 tx/skill/7/huo2_08 rotate: false xy: 512, 501 size: 199, 109 orig: 240, 160 offset: 19, 26 index: -1 tx/skill/7/huo2_10 rotate: false xy: 2, 436 size: 191, 110 orig: 240, 160 offset: 24, 26 index: -1 tx/skill/7/huo2_12 rotate: true xy: 906, 399 size: 196, 110 orig: 240, 160 offset: 22, 26 index: -1 tx/skill/7/huo2_14 rotate: false xy: 599, 373 size: 197, 110 orig: 240, 160 offset: 20, 26 index: -1 tx/skill/8/lizi_00 rotate: false xy: 399, 737 size: 64, 61 orig: 100, 100 offset: 13, 8 index: -1 tx/skill/8/lizi_02 rotate: true xy: 563, 728 size: 62, 70 orig: 100, 100 offset: 15, 12 index: -1 tx/skill/8/lizi_04 rotate: true xy: 127, 751 size: 60, 65 orig: 100, 100 offset: 14, 10 index: -1 tx/skill/8/lizi_06 rotate: true xy: 194, 751 size: 60, 76 orig: 100, 100 offset: 14, 7 index: -1 tx/skill/8/lizi_08 rotate: true xy: 902, 768 size: 59, 87 orig: 100, 100 offset: 15, 6 index: -1 tx/skill/9/ju_00 rotate: false xy: 2, 1018 size: 4, 4 orig: 100, 100 offset: 66, 42 index: -1 tx/skill/9/ju_01 rotate: false xy: 116, 997 size: 39, 25 orig: 100, 100 offset: 33, 40 index: -1 tx/skill/9/ju_02 rotate: false xy: 409, 913 size: 42, 41 orig: 100, 100 offset: 30, 28 index: -1 tx/skill/9/ju_03 rotate: true xy: 517, 912 size: 41, 55 orig: 100, 100 offset: 31, 16 index: -1 tx/skill/9/ju_04 rotate: true xy: 400, 862 size: 48, 59 orig: 100, 100 offset: 22, 16 index: -1 tx/skill/9/ju_05 rotate: true xy: 574, 910 size: 42, 55 orig: 100, 100 offset: 26, 18 index: -1 tx/skill/9/ju_06 rotate: true xy: 962, 936 size: 40, 53 orig: 100, 100 offset: 27, 19 index: -1 tx/skill/9/ju_07 rotate: true xy: 613, 954 size: 36, 49 orig: 100, 100 offset: 30, 22 index: -1 tx/skill/9/ju_08 rotate: true xy: 2, 969 size: 34, 45 orig: 100, 100 offset: 31, 25 index: -1 tx/skill/9/ju_09 rotate: true xy: 746, 992 size: 30, 33 orig: 100, 100 offset: 33, 32 index: -1 tx/skill/9/ju_10 rotate: false xy: 60, 1001 size: 24, 21 orig: 100, 100 offset: 36, 39 index: -1 tx/skill/9/ju_11 rotate: false xy: 39, 1005 size: 19, 17 orig: 100, 100 offset: 39, 41 index: -1 tx/skill/9/ju_12 rotate: false xy: 8, 1010 size: 12, 12 orig: 100, 100 offset: 43, 44 index: -1 ty rotate: false xy: 402, 994 size: 70, 28 orig: 74, 31 offset: 2, 1 index: -1 wb rotate: true xy: 341, 145 size: 124, 204 orig: 124, 204 offset: 0, 0 index: -1 ``` png图集  完整脚本 ``` import os from PIL import Image def parse_atlas(atlas_file_path): texture_regions = {} current_texture = None skipLine = 0 with open(atlas_file_path, 'r', encoding='utf-8') as file: lines = file.readlines() for index, line in enumerate(lines): if skipLine>0: skipLine-=1 continue line = line.strip() if not line: continue if line.endswith('.png'): current_texture = line texture_regions[current_texture] = [] elif line.startswith('size:'): continue elif line.startswith('format:'): continue elif line.startswith('filter:'): continue elif line.startswith('repeat:'): continue else: region_name = line region = { "name": region_name } skipLine = 0 while True: if index+1 >= len(lines): break sub_line = lines[index+1].strip() if not sub_line: break parts = sub_line.split(':') if len(parts) < 2: break index+=1 skipLine+=1 key = parts[0].strip() value = parts[1].strip() if key == 'rotate': region[key] = value == 'true' elif key in ['xy','size', 'orig', 'offset']: x, y = map(int, value.split(',')) region[key] = (x, y) elif key == 'index': region[key] = int(value) texture_regions[current_texture].append(region) return texture_regions def extract_sub_images(atlas_image_path, sub_images_info, output_dir): atlas_image = Image.open(atlas_image_path) if not os.path.exists(output_dir): os.makedirs(output_dir) for pngname,regions in sub_images_info.items(): for region in regions: name = region['name'] rotate = region['rotate'] xy = region['xy'] size = region['size'] orig = region['orig'] offset = region['offset'] # 计算裁剪区域 if rotate: x, y = xy w, h = size[1], size[0] else: x, y = xy w, h = size cropped_image = atlas_image.crop((x, y, x + w, y + h)) # 调整到原始大小 new_image = Image.new('RGBA', orig) if rotate: cropped_image = cropped_image.transpose(Image.ROTATE_270) new_image.paste(cropped_image,(offset[0],orig[1]-offset[1]-w)) else: #offset的解释:需要往图片[左方]和[下方]增加的空白区域尺寸 new_image.paste(cropped_image,(offset[0],orig[1]-offset[1]-h)) # 保存图片 name = os.path.join(output_dir, name) dir = os.path.dirname(name) if not os.path.exists(dir): os.makedirs(dir) new_image.save(f"{name}.png") if __name__ == "__main__": atlas_file_path = "15001.atlas" atlas_image_path = "15001.png" output_dir = "extracted_images" sub_images = parse_atlas(atlas_file_path) extract_sub_images(atlas_image_path, sub_images, output_dir) ``` **注意:里面的offset参数是个坑点,一直以为是size在orig居于左上角的偏移,其实是左下角** 拆分后的内容  标签: none