Skip to content

Instantly share code, notes, and snippets.

@minyoung
Created March 11, 2025 05:52
Show Gist options
  • Save minyoung/22d3348ca3a99c9ab8c8a26ad4675eb2 to your computer and use it in GitHub Desktop.
Save minyoung/22d3348ca3a99c9ab8c8a26ad4675eb2 to your computer and use it in GitHub Desktop.
Migrate old darkest dungeon 1 skins to new atlas offsets
{
"size": [645, 548],
"transforms": [
{
"source": [2, 13, 92, 163],
"rotate": -90,
"target": [487, 333]
},
{
"source": [133, 48, 213, 173],
"rotate": -90,
"target": [478, 466]
},
{
"source": [217, 22, 308, 181],
"rotate": -90,
"target": [257, 389, 416, 480]
},
{
"source": [311, 25, 447, 167],
"target": [112, 326, 248, 468]
},
{
"source": [450, 5, 516, 53],
"target": [183, 44]
},
{
"source": [467, 56, 668, 173],
"rotate": -90,
"target": [118, 111, 235, 312]
},
{
"source": [523, 16, 592, 53],
"target": [28, 11, 97, 48]
},
{
"source": [595, 14, 618, 53],
"target": [2, 9, 25, 48]
},
{
"source": [671, 3, 749, 58],
"rotate": 90,
"target": [574, 31, 629, 109]
},
{
"source": [671, 61, 770, 179],
"rotate": -90,
"target": [257, 298, 375, 397]
},
{
"source": [759, 5, 807, 55],
"target": [577, 112, 625, 162]
},
{
"source": [773, 63, 871, 179],
"rotate": -90,
"target": [257, 200, 373, 298]
},
{
"source": [818, 3, 907, 60],
"rotate": -90,
"target": [112, 2, 169, 91]
},
{
"source": [879, 70, 981, 177],
"target": [471, 205, 573, 312]
},
{
"source": [910, 3, 972, 67],
"rotate": -90,
"target": [576, 168, 640, 230]
},
{
"source": [975, 5, 1331, 67],
"target": [112, 483, 468, 545]
},
{
"source": [990, 72, 1312, 179],
"rotate": -90,
"target": [2, 54, 109, 376]
},
{
"source": [1336, 7, 1402, 69],
"rotate": 90,
"target": [581, 246, 643, 312]
},
{
"source": [1420, 2, 1493, 69],
"target": [394, 413, 467, 480]
},
{
"source": [1421, 102, 1477, 150],
"rotate": -90,
"target": [31, 489, 79, 545]
},
{
"source": [1480, 74, 1583, 179],
"rotate": -90,
"target": [236, 95, 341, 198]
},
{
"source": [1498, 14, 1568, 66],
"rotate": -90,
"target": [509, 132, 561, 202]
},
{
"source": [1583, 5, 1656, 71],
"target": [392, 344, 465, 410]
},
{
"source": [1587, 83, 1666, 179],
"rotate": 90,
"target": [402, 22, 498, 101]
},
{
"source": [1659, 2, 1736, 72],
"rotate": 90,
"target": [376, 200, 446, 277]
},
{
"source": [1669, 102, 1728, 179],
"rotate": -90,
"target": [378, 280, 455, 339]
},
{
"source": [1733, 94, 1784, 168],
"target": [258, 7, 309, 81]
},
{
"source": [1739, 20, 1818, 91],
"target": [312, 21, 391, 92]
},
{
"source": [1787, 102, 1865, 179],
"target": [422, 120, 500, 197]
},
{
"source": [1821, 25, 1876, 83],
"target": [501, 43, 556, 101]
},
{
"source": [1872, 121, 1917, 166],
"rotate": -90,
"target": [352, 129]
},
{
"source": [1895, 43, 1966, 103],
"rotate": -90,
"target": [526, 316]
}
]
}
#!/usr/bin/env python
import json
import sys
from PIL import Image
def dissect(filename, export_sprites=True):
image = Image.open(filename)
size = image.size
print('Image size:', size)
seen = set()
def find_pixel(resume):
x, y = resume
while x < size[0]:
while y < size[1]:
coord = (x, y)
if coord in seen:
y += 1
continue
if image.getpixel(coord):
return coord
y += 1
y = 0
x += 1
return None
sprite_start = (0, 0)
sprites = []
while True:
sprite_start = find_pixel(sprite_start)
if sprite_start is None:
break
sprite_bbox = [None, None, None, None]
def check_coord(coord):
if coord in seen:
return None
for i in range(2):
if coord[i] < 0:
return None
if coord[i] >= size[i]:
return None
seen.add(coord)
pixel = image.getpixel(coord)
if not pixel or pixel[3] == 0:
return None
for i in range(2):
if sprite_bbox[i] is None or sprite_bbox[i] > coord[i]:
sprite_bbox[i] = coord[i]
if sprite_bbox[2+i] is None or sprite_bbox[2+i] < coord[i]:
sprite_bbox[2+i] = coord[i]
return coord
seen.add(sprite_start)
queue = [sprite_start]
while queue:
coord = queue.pop()
for offset in [[-1, 0], [1, 0], [0, -1], [0, 1]]:
checked = check_coord((coord[0] + offset[0], coord[1] + offset[1]))
if checked:
queue.append(checked)
if sprite_bbox[0] is None:
continue
width = sprite_bbox[2] - sprite_bbox[0]
height = sprite_bbox[3] - sprite_bbox[1]
if width < 5 or height < 5:
continue
# print("sprite found:", sprite_bbox)
if export_sprites:
sprite = image.crop(sprite_bbox)
sprite.save('sprite-{}-{}-{}-{}.png'.format(*sprite_bbox))
sprites.append(sprite_bbox)
print("{")
print(' "size": [{}, {}],'.format(*size))
print(' "transforms": [')
for sprite in sprites:
print(' {')
# print(' "source": [],')
print(' "source": [{}, {}, {}, {}],'.format(*sprite))
print(' "rotate": [],')
print(' "target": []')
# print(' "target": [{}, {}, {}, {}]'.format(*sprite))
print(' },')
print(' ]')
print('}')
# TODO? inline image edit
def reconstruct(input_image_filename, output_image_filename, rules_filename):
rules = {}
with open(rules_filename) as f:
rules = json.load(f)
image = Image.open(input_image_filename)
# output_image = Image.new('RGBA', rules['size'], None)
output_image = Image.new(image.mode, rules['size'], None)
for rule in rules['transforms']:
sprite = image.crop(rule['source'])
if not rule['target']:
continue
if 'rotate' in rule and rule['rotate']:
if rule['rotate'] == 90:
sprite = sprite.transpose(Image.ROTATE_90)
elif rule['rotate'] == -90:
sprite = sprite.transpose(Image.ROTATE_270)
else:
sprite = sprite.rotate(rule['rotate'], expand=True)
# sprite = sprite.crop(sprite.getbbox())
print('Pasting:', rule, sprite.size)
overlay = Image.new(image.mode, rules['size'], None)
overlay.paste(sprite, rule['target'])
output_image = Image.alpha_composite(output_image, overlay)
# output_image.paste(sprite, rule['target'])
output_image.save(output_image_filename)
# dissect(sys.argv[1])
reconstruct(sys.argv[1], sys.argv[2], sys.argv[3])
{
"size": [1239, 271],
"transforms": [
{
"source": [962, 52, 1054, 217],
"target": [251, 98]
},
{
"source": [3, 4, 216, 219],
"rotate": -90,
"target": [81, 59]
},
{
"source": [715, 47, 958, 182],
"rotate": -90,
"target": [38, 20]
},
{
"source": [906, 11, 950, 44],
"target": [17, 200]
},
{
"source": [953, 16, 1013, 44],
"target": [2, 230]
},
{
"source": [1057, 3, 1135, 51],
"target": [1119, 155]
},
{
"source": [1315, 89, 1369, 138],
"rotate": -90,
"target": [1186, 147]
},
{
"source": [1383, 101, 1483, 152],
"rotate": 90,
"target": [888, 103]
},
{
"source": [1584, 96, 1634, 152],
"rotate": 90,
"target": [876, 153]
},
{
"source": [302, 21, 479, 209],
"target": [434, 80, 611, 268]
},
{
"source": [484, 33, 608, 208],
"target": [645, 93, 769, 268]
},
{
"source": [215, 6, 251, 32],
"target": [346, 57, 382, 83]
},
{
"source": [230, 63, 301, 217],
"target": [361, 114, 432, 268]
},
{
"source": [623, 2, 654, 40],
"rotate": -90,
"target": [1198, 110, 1236, 141]
},
{
"source": [862, 5, 903, 44],
"target": [1193, 71, 1234, 110]
},
{
"source": [624, 43, 712, 217],
"target": [785, 94, 873, 268]
},
{
"source": [657, 4, 754, 40],
"rotate": 90,
"target": [931, 3, 967, 100]
},
{
"source": [758, 8, 855, 44],
"rotate": 90,
"target": [944, 106, 980, 203]
},
{
"source": [1057, 54, 1183, 214],
"rotate": 90,
"target": [1031, 9, 1191, 135]
},
{
"source": [1185, 3, 1268, 69],
"rotate": 90,
"target": [642, 9, 708, 92]
},
{
"source": [1192, 76, 1239, 220],
"rotate": -90,
"target": [345, 7, 489, 54]
},
{
"source": [1242, 87, 1306, 223],
"rotate": -90,
"target": [983, 138, 1119, 202]
},
{
"source": [1278, 2, 1345, 81],
"target": [776, 9, 843, 88]
},
{
"source": [1310, 155, 1666, 217],
"target": [876, 206, 1232, 268]
},
{
"source": [1356, 26, 1435, 84],
"rotate": -90,
"target": [712, 9, 770, 88]
},
{
"source": [1443, 11, 1518, 90],
"target": [852, 12, 927, 91]
},
{
"source": [1489, 96, 1584, 155],
"rotate": 90,
"target": [971, 13, 1030, 108]
},
{
"source": [1521, 7, 1592, 91],
"target": [494, 2, 565, 86]
},
{
"source": [1596, 13, 1678, 82],
"rotate": 90,
"target": [569, 5, 638, 87]
}
]
}
{
"size": [1544, 247],
"transforms": [
{
"source": [2, 23, 201, 183],
"rotate": -90,
"target": [262, 45, 422, 244]
},
{
"source": [2, 186, 174, 277],
"target": [425, 153, 597, 244]
},
{
"source": [206, 6, 358, 182],
"rotate": 90,
"target": [784, 8, 960, 160]
},
{
"source": [228, 198, 334, 251],
"target": [649, 165, 755, 218]
},
{
"source": [337, 215, 693, 277],
"target": [425, 88, 781, 150]
},
{
"source": [362, 17, 518, 81],
"target": [760, 180, 916, 244]
},
{
"source": [362, 84, 519, 212],
"rotate": 90,
"target": [964, 11, 1092, 168]
},
{
"source": [522, 12, 608, 93],
"target": [61, 164, 147, 245]
},
{
"source": [525, 96, 688, 208],
"rotate": -90,
"target": [1099, 18, 1211, 181]
},
{
"source": [611, 23, 674, 93],
"target": [1402, 10, 1465, 80]
},
{
"source": [677, 24, 744, 89],
"rotate": -90,
"target": [1468, 13, 1533, 80]
},
{
"source": [696, 180, 874, 277],
"rotate": -90,
"target": [1214, 3, 1311, 181]
},
{
"source": [699, 92, 799, 173],
"rotate": -90,
"target": [1318, 49, 1399, 149]
},
{
"source": [747, 42, 798, 89],
"rotate": 90,
"target": [472, 2, 519, 53]
},
{
"source": [801, 12, 848, 41],
"target": [425, 56, 472, 85]
},
{
"source": [801, 44, 873, 74],
"target": [262, 12, 334, 42]
},
{
"source": [802, 77, 875, 176],
"rotate": 90,
"target": [919, 171, 1018, 244]
},
{
"source": [851, 2, 893, 26],
"rotate": 90,
"target": [1257, 202, 1281, 244]
},
{
"source": [874, 183, 988, 276],
"target": [1312, 152, 1426, 245]
},
{
"source": [878, 109, 988, 182],
"target": [574, 12, 684, 85]
},
{
"source": [894, 47, 972, 106],
"target": [1418, 90, 1496, 149]
},
{
"source": [898, 12, 924, 37],
"rotate": 90,
"target": [1284, 218, 1309, 244]
},
{
"source": [991, 37, 1085, 119],
"target": [687, 3, 781, 85]
},
{
"source": [991, 123, 1103, 180],
"target": [1142, 185, 1254, 242]
},
{
"source": [996, 185, 1104, 277],
"target": [1433, 152, 1541, 244]
},
{
"source": [1088, 29, 1147, 62],
"target": [339, 6, 398, 39]
},
{
"source": [1088, 68, 1137, 119],
"target": [522, 8, 571, 59]
},
{
"source": [1106, 122, 1165, 157],
"target": [401, 7, 460, 42]
},
{
"source": [1107, 160, 1167, 277],
"rotate": -90,
"target": [1022, 184, 1139, 244]
},
{
"source": [1140, 75, 1163, 119],
"rotate": 90,
"target": [478, 62, 522, 85]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment