Skip to content

Instantly share code, notes, and snippets.

@flyover
Last active March 15, 2017 17:06
Spine Import/Export Plugin for GIMP
#!/usr/bin/env python
# pylint: disable=E0401
# pylint: disable=E0602
# pylint: disable=W0401
# pylint: disable=W0613
# pylint: disable=C0103
# pylint: disable=C0111
# pylint: disable=C0301
# pylint: disable=C0330
'''
Spine Import/Export Plugin
Use the following GIMP layer hierarchy:
bones gimp.LayerGroup
root gimp.Layer
bone1 parent(root) gimp.Layer
bone2 parent(bone1) gimp.Layer
slots gimp.LayerGroup
slot1 bone(bone1) gimp.Layer
slot2 bone(bone2) gimp.Layer
skins gimp.LayerGroup
default gimp.LayerGroup
attachment1-1 slot(slot1) gimp.Layer
attachment1-2 slot(slot1) gimp.Layer
attachment2 slot(slot2) gimp.Layer
skin1 gimp.LayerGroup
skin1-attachment1-1 slot(slot1) gimp.Layer
skin1-attachment1-2 slot(slot1) gimp.Layer
skin1-attachment2 slot(slot2) gimp.Layer
'''
import json
import os.path
import re
from gimpfu import *
def console_log(message):
pdb.gimp_message(message)
def find_layer(layers, name, recurse=False):
for layer in layers:
if layer.name == name:
return layer
if recurse and isinstance(layer, gimp.GroupLayer):
layer = find_layer(layer.layers, name, recurse)
if layer:
return layer
return None
def find_bone(bones, name):
for bone in bones:
if bone.get('name') == name:
return bone
return None
def find_slot(slots, name):
for slot in slots:
if slot.get('name') == name:
return slot
return None
def spine_import(image, active_drawable):
''' Spine Import Plugin entry point
'''
image.undo_group_start()
image_dirname = os.path.dirname(image.filename)
image_basename = os.path.basename(image.filename)
image_splitext = os.path.splitext(image_basename)
image_rootname = image_splitext[0]
spine_json_basename = '%s.json' % image_rootname
spine_json_filename = os.path.join(image_dirname, spine_json_basename)
with open(spine_json_filename, 'r') as spine_json_file:
spine_json = json.load(spine_json_file)
bones = spine_json.get('bones')
slots = spine_json.get('slots')
skins = spine_json.get('skins')
bones_group = find_layer(image.layers, 'bones')
slots_group = find_layer(image.layers, 'slots')
skins_group = find_layer(image.layers, 'skins')
if not bones_group:
bones_group = gimp.GroupLayer(image, 'bones')
image.add_layer(bones_group, 0)
if not slots_group:
slots_group = gimp.GroupLayer(image, 'slots')
image.add_layer(slots_group, image.layers.index(bones_group) + 1)
if not skins_group:
skins_group = gimp.GroupLayer(image, 'skins')
image.add_layer(skins_group, image.layers.index(slots_group) + 1)
for bone in bones:
bone_key = bone.get('name')
parent_bone_key = bone.get('parent') or 'root'
x = bone.get('x') or 0.0
y = bone.get('y') or 0.0
w = 48
h = 48
# convert from local space to world space
parent_bone = find_bone(bones, bone.get('parent'))
while parent_bone:
x += parent_bone.get('x') or 0.0
y += parent_bone.get('y') or 0.0
parent_bone = find_bone(bones, parent_bone.get('parent'))
# convert from Spine space to GIMP space
ox = (x + image.width/2) - w/2
oy = (image.height/2 - y) - h/2
bone_layer_name = '%s parent(%s)' % (bone_key, parent_bone_key)
console_log('bone layer %s at %d,%d (%dx%d)' % (bone_layer_name, ox, oy, w, h))
bone_layer = find_layer(bones_group.layers, bone_layer_name)
if not bone_layer:
bone_layer = gimp.Layer(image, bone_layer_name, w, h, RGBA_IMAGE, 100, NORMAL_MODE)
bone_layer.name = bone_layer_name
image.insert_layer(bone_layer, bones_group, len(bones_group.layers))
bone_layer.set_offsets(ox, oy)
pdb.gimp_edit_fill(bone_layer, FOREGROUND_FILL)
for slot in slots:
slot_key = slot.get('name')
bone_key = slot.get('bone') or 'root'
slot_layer_name = '%s bone(%s)' % (slot_key, bone_key)
console_log('slot layer %s' % (slot_layer_name))
slot_layer = find_layer(slots_group.layers, slot_layer_name)
if not slot_layer:
slot_layer = gimp.Layer(image, slot_layer_name, w, h, RGBA_IMAGE, 100, NORMAL_MODE)
slot_layer.name = slot_layer_name
image.insert_layer(slot_layer, slots_group, len(slots_group.layers))
for skin_key, skin in skins.items():
skin_group_name = '%s' % (skin_key)
console_log('skin group %s' % (skin_group_name))
skin_group = find_layer(skins_group.layers, skin_group_name)
if not skin_group:
skin_group = gimp.GroupLayer(image, skin_group_name)
image.insert_layer(skin_group, skins_group, len(skins_group.layers))
for slot_key, skin_slot in skin.items():
for attachment_key, attachment in skin_slot.items():
slot = find_slot(slots, slot_key)
bone = find_bone(bones, slot.get('bone'))
x = 0
y = 0
w = attachment.get('width')
h = attachment.get('height')
# convert from local space to world space
while bone:
x += bone.get('x') or 0.0
x += bone.get('y') or 0.0
bone = find_bone(bones, bone.get('parent'))
x += attachment.get('x') or 0.0
y += attachment.get('y') or 0.0
# convert from Spine space to GIMP space
ox = (x + image.width/2) - w/2
oy = (image.height/2 - y) - h/2
attachment_layer_name = '%s slot(%s)' % (attachment_key, slot_key)
console_log('attachment layer %s at %d,%d (%dx%d)' % (attachment_layer_name, ox, oy, w, h))
attachment_layer = find_layer(skin_group.layers, attachment_layer_name)
if not attachment_layer:
attachment_basename = '%s.png' % attachment_key
attachment_filename = os.path.join(image_dirname, attachment_basename)
attachment_layer = pdb.gimp_file_load_layer(image, attachment_filename)
if attachment_layer:
attachment_layer.name = attachment_layer_name
attachment_layer.set_offsets(ox, oy)
image.insert_layer(attachment_layer, skin_group, len(skin_group.layers))
if not attachment_layer:
attachment_layer = gimp.Layer(image, attachment_layer_name, w, h, RGBA_IMAGE, 100, NORMAL_MODE)
attachment_layer.set_offsets(ox, oy)
image.insert_layer(attachment_layer, skin_group, len(skin_group.layers))
pdb.gimp_edit_fill(attachment_layer, FOREGROUND_FILL)
image.undo_group_end()
def spine_export(image, active_drawable):
''' Spine Export Plugin entry point
'''
image.undo_group_start()
image_dirname = os.path.dirname(image.filename)
image_basename = os.path.basename(image.filename)
image_splitext = os.path.splitext(image_basename)
image_rootname = image_splitext[0]
spine_json_basename = '%s.json' % image_rootname
spine_json_filename = os.path.join(image_dirname, spine_json_basename)
with open(spine_json_filename, 'w') as spine_json_file:
spine_json = {
'bones': [{'name': 'root'}],
'slots': [],
'skins': {}
}
bones = spine_json.get('bones')
slots = spine_json.get('slots')
skins = spine_json.get('skins')
bones_group = find_layer(image.layers, 'bones')
slots_group = find_layer(image.layers, 'slots')
skins_group = find_layer(image.layers, 'skins')
for bone_layer in bones_group.layers:
array = re.split(r'\s+', bone_layer.name)
bone_key = array[0]
parent_bone_key = None
for token in array:
parent_match = re.match(r'parent\(([\w\-]+)\)', token)
if parent_match:
parent_bone_key = parent_match.group(1)
else:
bone_key = token
if not parent_bone_key and bone_key != 'root':
parent_bone_key = 'root'
tmp_image = pdb.gimp_image_new(image.width, image.height, image.base_type)
tmp_layer = pdb.gimp_layer_new_from_drawable(bone_layer, tmp_image)
tmp_image.add_layer(tmp_layer, 0)
pdb.gimp_image_set_active_layer(tmp_image, tmp_layer)
pdb.plug_in_autocrop_layer(tmp_image, tmp_layer)
# save offset before resizing to layer
ox, oy = tmp_layer.offsets
tmp_image.resize_to_layers()
w = tmp_layer.width
h = tmp_layer.height
pdb.gimp_image_delete(tmp_image)
# convert from GIMP space to Spine space
x = (ox + w/2) - image.width/2
y = image.height/2 - (oy + h/2)
# convert from world space to local space
bone = find_bone(bones, parent_bone_key)
while bone:
x -= bone.get('x') or 0.0
y -= bone.get('y') or 0.0
bone = find_bone(bones, bone.get('parent'))
bone = find_bone(bones, bone_key)
if not bone:
bone = {
'name': bone_key,
'parent': parent_bone_key
}
bones.append(bone)
bone['x'] = x
bone['y'] = y
for slot_layer in slots_group.layers:
array = re.split(r'\s+', slot_layer.name)
slot_key = array[0]
bone_key = None
for token in array:
bone_match = re.match(r'bone\(([\w\-]+)\)', token)
if bone_match:
bone_key = bone_match.group(1)
else:
slot_key = token
if not bone_key:
bone_key = 'root'
slot = find_slot(slots, slot_key)
if not slot:
slot = {
'name': slot_key,
'bone': bone_key
}
slots.append(slot)
for skin_group in skins_group.layers:
skin_key = skin_group.name
skin = skins[skin_key] = {}
for attachment_layer in skin_group.layers:
array = re.split(r'\s+', attachment_layer.name)
attachment_key = array[0]
bone_key = None
slot_key = None
for token in array:
bone_match = re.match(r'bone\(([\w\-]+)\)', token)
slot_match = re.match(r'slot\(([\w\-]+)\)', token)
if bone_match:
bone_key = bone_match.group(1)
elif slot_match:
slot_key = slot_match.group(1)
else:
attachment_key = token
if not bone_key:
bone_key = 'root'
if not slot_key:
slot_key = attachment_key
slot = find_slot(slots, slot_key)
if not slot:
slot = {
'name': slot_key,
'bone': bone_key
}
slots.append(slot)
if 'attachment' not in slot:
slot['attachment'] = attachment_key
tmp_image = pdb.gimp_image_new(image.width, image.height, image.base_type)
tmp_layer = pdb.gimp_layer_new_from_drawable(attachment_layer, tmp_image)
tmp_image.add_layer(tmp_layer, 0)
pdb.gimp_image_set_active_layer(tmp_image, tmp_layer)
pdb.plug_in_autocrop_layer(tmp_image, tmp_layer)
ox, oy = tmp_layer.offsets
tmp_image.resize_to_layers()
w = tmp_layer.width
h = tmp_layer.height
attachment_basename = '%s.png' % attachment_key
attachment_filename = os.path.join(image_dirname, attachment_basename)
pdb.file_png_save(tmp_image, tmp_layer, attachment_filename, attachment_basename,
0, # interlace
0, # compression
1, # bkgd
1, # gama
1, # offs
1, # phys
1 # time
)
pdb.gimp_image_delete(tmp_image)
# convert from GIMP space to Spine space
x = (ox + w/2) - image.width/2
y = image.height/2 - (oy + h/2)
# convert from world space to local space
slot = find_slot(slots, slot_key)
bone = find_bone(bones, slot.get('bone'))
while bone:
x -= bone.get('x') or 0.0
y -= bone.get('y') or 0.0
bone = find_bone(bones, bone.get('parent'))
skin_slot = skin.setdefault(slot_key, {})
attachment = {
'x': x,
'y': y,
'width': w,
'height': h,
}
skin_slot[attachment_key] = attachment
json.dump(spine_json, spine_json_file, indent=4)
image.undo_group_end()
register(
# name
"spine-import",
# blurb
"Spine Import",
# help
"Imports a Spine JSON file",
# author
"Isaac Burns",
# copyright
"Flyover Games, LLC",
# date
"2015",
# menupath
"<Image>/File/Export/Spine/Spine Import",
# imagetypes
"*",
# params
[],
# results
[],
# function
spine_import
)
register(
# name
"spine-export",
# blurb
"Spine Export",
# help
"Exports a Spine JSON file",
# author
"Isaac Burns",
# copyright
"Flyover Games, LLC",
# date
"2015",
# menupath
"<Image>/File/Export/Spine/Spine Export",
# imagetypes
"*",
# params
[],
# results
[],
# function
spine_export
)
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment