Last active
March 15, 2017 17:06
Spine Import/Export Plugin for GIMP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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