Skip to content
Snippets Groups Projects
io_import_gimp_image_to_scene.py 24.47 KiB
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

bl_addon_info = {
    "name": "Import GIMP Image to Scene (.xcf, .xjt)",
    "author": "Daniel Salazar (ZanQdo)",
    "version": (2, 0, 0),
    "blender": (2, 5, 5),
    "api": 33419,
    "location": "File > Import > GIMP Image to Scene(.xcf, .xjt)",
    "description": "Imports GIMP multilayer image files into 3D Layers",
    "warning": "XCF import requires xcftools installed",
    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\
        "Scripts/Import-Export/GIMPImageToScene",
    "tracker_url": "http://projects.blender.org/tracker/index.php?"\
        "func=detail&aid=25136",
    "category": "Import-Export"}

"""
This script imports GIMP layered image files into 3D Scenes (.xcf, .xjt)
"""

def main(File, Path, LayerViewers, MixerViewers, LayerOffset,\
    LayerScale, OpacityMode, PremulAlpha, ShadelessMats,\
    SetCamera, SetupCompo, GroupUntagged, Ext):
    
    #-------------------------------------------------
    
    #Folder = '['+File.rstrip(Ext)+']'+'_images/'
    Folder = 'images_'+'['+File.rstrip(Ext)+']/'
    
    if bpy.data.is_dirty:
        PathSaveRaw = Path+Folder
        PathSave = PathSaveRaw.replace(' ', '\ ')
        try: os.mkdir(PathSaveRaw)
        except: pass
    else:
        PathSave = bpy.data.filepath
        RSlash = PathSave.rfind('/')
        PathSaveRaw = PathSave[:RSlash+1]+Folder
        PathSave = PathSaveRaw.replace(' ', '\ ')
        try: os.mkdir(PathSaveRaw)
        except: pass
        PathSaveRaw = bpy.path.relpath(PathSaveRaw)+'/'
    
    PathRaw = Path
    Path = Path.replace(' ', '\ ')
    if Ext == '.xjt':
        ExtSave = '.jpg'
        #-------------------------------------------------
        # EXTRACT XJT
        import tarfile
        
        IMG = tarfile.open ('%s%s' % (PathRaw, File))
        PRP = IMG.extractfile('PRP')
        
        Members = IMG.getmembers()
        
        for Member in Members:
            Name = Member.name
            if Name.startswith('l') and Name.endswith('.jpg'):
                IMG.extract(Name, path=PathSaveRaw)
        
        #-------------------------------------------------
        # INFO XJT
        IMGs = []
        for Line in PRP.readlines():
            Line = str(Line)
            
            if Line.startswith("b'GIMP_XJ_IMAGE"):
                for Segment in Line.split():
                    if Segment.startswith('w/h:'):
                        ResX, ResY = map (int, Segment[4:].split(','))
            if Line.startswith("b'L") or Line.startswith("b'l"):
                
                if Line.startswith("b'L"): HasAlpha = True
                else: HasAlpha = False
                
                md = None
                op = 1
                ox, oy = 0,0
                
                for Segment in Line.split():
                    
                    if Segment.startswith("b'"):
                        imageFile = 'l' + Segment[3:] + '.jpg'
                        imageFileAlpha ='la'+Segment[3:]+'.jpg'
                        
                        # Get Widht and Height from images
                        data = open(PathSaveRaw+imageFile, "rb").read()
                        
                        hexList = []
                        for ch in data:
                            byt = "%02X" % ch
                            hexList.append(byt)
                        
                        for k in range(len(hexList)-1):
                            if hexList[k] == 'FF' and (hexList[k+1] == 'C0' or hexList[k+1] == 'C2'):
                                ow = int(hexList[k+7],16)*256 + int(hexList[k+8],16)
                                oh = int(hexList[k+5],16)*256 + int(hexList[k+6],16)
                    
                    elif Segment.startswith('md:'): # mode
                        md = Segment[3:]
                        
                    elif Segment.startswith('op:'): # opacity
                        op = float(Segment[3:])*.01
                    
                    elif Segment.startswith('o:'): # origin
                        ox, oy = map(int, Segment[2:].split(','))
                    
                    elif Segment.startswith('n:'): # name
                        n = Segment[3:-4]
                        OpenBracket = n.find ('[')
                        CloseBracket = n.find (']')
                        
                        if OpenBracket != -1 and CloseBracket != -1:
                            RenderLayer = n[OpenBracket+1:CloseBracket]
                            NameShort = n[:OpenBracket]
                            
                        else:
                            RenderLayer = n
                            NameShort = n
                        
                        os.rename(PathSaveRaw+imageFile, PathSaveRaw+NameShort+'.jpg')
                        if HasAlpha: os.rename(PathSaveRaw+imageFileAlpha, PathSaveRaw+NameShort+'_A'+'.jpg')
                        
                IMGs.append({'LayerMode':md, 'LayerOpacity':op,\
                            'LayerName':n, 'LayerNameShort':NameShort,\
                            'RenderLayer':RenderLayer, 'LayerCoords':[ow, oh, ox, oy], 'HasAlpha':HasAlpha})
    
    else: # Ext == '.xcf':
        ExtSave = '.png'
        #-------------------------------------------------
        # CONFIG
        XCFInfo = 'xcfinfo'
        XCF2PNG = 'xcf2png'
        #-------------------------------------------------
        # INFO XCF
        
        CMD = '%s %s%s' % (XCFInfo, Path, File)
        
        Info = os.popen(CMD)
        
        IMGs = []
        for Line in Info.readlines():
            if Line.startswith ('+'):
                
                Line = Line.split(' ', 4)
                
                RenderLayer = Line[4]
                
                OpenBracket = RenderLayer.find ('[')
                CloseBracket = RenderLayer.find (']')
                
                if OpenBracket != -1 and CloseBracket != -1:
                    RenderLayer = RenderLayer[OpenBracket+1:CloseBracket]
                    NameShort = Line[4][:OpenBracket]
                else:
                    NameShort = Line[4].rstrip()
                    if GroupUntagged:
                        RenderLayer = '__Undefined__'
                    else:
                        RenderLayer = NameShort
                
                LineThree = Line[3]
                Slash = LineThree.find('/')
                if Slash == -1:
                    Mode = LineThree
                    Opacity = 1
                else:
                    Mode = LineThree[:Slash]
                    Opacity = float(LineThree[Slash+1:LineThree.find('%')])*.01
                
                IMGs.append ({\
                    'LayerMode':Mode,\
                    'LayerOpacity':Opacity,\
                    'LayerName':Line[4].rstrip(),\
                    'LayerNameShort':NameShort,\
                    'LayerCoords':list(map(int, Line[1].replace('x', ' ').replace('+', ' +').replace('-', ' -').split())),\
                    'RenderLayer':RenderLayer,\
                    'HasAlpha':True,\
                    })
            elif Line.startswith('Version'):
                ResX, ResY = map (int, Line.split()[2].split('x'))
        
        #-------------------------------------------------
        # EXTRACT XCF
        if OpacityMode == 'BAKE':
            Opacity = ''
        else:
            Opacity = ' --percent 100'
        for Layer in IMGs:
            CMD = '%s -C %s%s -o %s%s.png "%s"%s' %\
            (XCF2PNG, Path, File, PathSave, Layer['LayerName'].replace(' ', '_'), Layer['LayerName'], Opacity)
            os.system(CMD)
    
    #-------------------------------------------------
    Scene = bpy.context.scene
    #-------------------------------------------------
    # CAMERA
    
    if SetCamera:
        bpy.ops.object.camera_add(location=(0, 0, 10))
        
        Camera = bpy.context.active_object.data
        
        Camera.type = 'ORTHO'
        Camera.ortho_scale = ResX * .01
    
    #-------------------------------------------------
    # RENDER SETTINGS
    
    Render = Scene.render
    
    if SetCamera:
        Render.resolution_x = ResX
        Render.resolution_y = ResY
        Render.resolution_percentage = 100
    if PremulAlpha: Render.alpha_mode = 'PREMUL'
    
    #-------------------------------------------------
    # 3D VIEW SETTINGS
    
    Scene.game_settings.material_mode = 'GLSL'
    
    Areas = bpy.context.screen.areas
    
    for Area in Areas:
        if Area.type == 'VIEW_3D':
            Area.active_space.viewport_shade = 'TEXTURED'
            Area.active_space.show_textured_solid = True
            Area.active_space.show_floor = False
    
    #-------------------------------------------------
    # 3D LAYERS
    
    def Make3DLayer (Name, NameShort, Z, Coords, RenderLayer, LayerMode, LayerOpacity, HasAlpha):
        
        # RenderLayer
        
        if SetupCompo:
            if not bpy.context.scene.render.layers.get(RenderLayer):
                
                bpy.ops.scene.render_layer_add()
                
                LayerActive = bpy.context.scene.render.layers.active
                LayerActive.name = RenderLayer
                LayerActive.use_pass_vector = True
                LayerActive.use_sky = False
                LayerActive.use_edge_enhance = False
                LayerActive.use_strand = False
                LayerActive.use_halo = False
                
                global LayerNum
                for i in range (0,20):
                    if not i == LayerNum:
                        LayerActive.layers[i] = False
                
                bpy.context.scene.layers[LayerNum] = True
                
                LayerFlags[RenderLayer] = bpy.context.scene.render.layers.active.layers
                
                LayerList.append([RenderLayer, LayerMode, LayerOpacity])
                
                LayerNum += 1
        
        # Object
        bpy.ops.mesh.primitive_plane_add(\
        view_align=False,\
        enter_editmode=False,\
        rotation=(0, 0, pi))
        
        bpy.ops.object.rotation_apply()
        
        Active = bpy.context.active_object
        
        if SetupCompo:
            Active.layers = LayerFlags[RenderLayer]
        
        Active.location = (\
            (float(Coords[2])-(ResX*0.5))*LayerScale,\
            (-float(Coords[3])+(ResY*0.5))*LayerScale, Z)
        
        for Vert in Active.data.vertices:
            Vert.co[0] += 1
            Vert.co[1] += -1
            
        Active.dimensions = float(Coords[0])*LayerScale, float(Coords[1])*LayerScale, 0
        
        bpy.ops.object.scale_apply()
        
        bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
        
        Active.show_wire = True
        
        Active.name = NameShort
        bpy.ops.mesh.uv_texture_add()
        
        # Material
        
        '''if bpy.data.materials.get(NameShort):
            Mat = bpy.data.materials[NameShort]
            if not Active.material_slots:
                bpy.ops.object.material_slot_add()
            Active.material_slots[0].material = Mat
        else:'''
        
        Mat = bpy.data.materials.new(NameShort)
        Mat.diffuse_color = (1,1,1)
        Mat.use_raytrace = False
        Mat.use_shadows = False
        Mat.use_cast_buffer_shadows = False
        Mat.use_cast_approximate = False
        if HasAlpha:
            Mat.use_transparency = True
            if OpacityMode == 'MAT': Mat.alpha = LayerOpacity
            else: Mat.alpha = 0
        if ShadelessMats: Mat.use_shadeless = True
        
        if Ext == '.xcf':
            # Color & Alpha PNG
            Tex = bpy.data.textures.new(NameShort, 'IMAGE')
            Tex.extension = 'CLIP'
            Tex.use_preview_alpha = True
            
            Img = bpy.data.images.new(NameShort)
            Img.source = 'FILE'
            if PremulAlpha: Img.use_premultiply = True
            Img.filepath = '%s%s%s' % (PathSaveRaw, Name, ExtSave)
            
            UVFace = Active.data.uv_textures[0].data[0]
            UVFace.image = Img
            UVFace.use_image = True
            
            Tex.image = Img
            
            Mat.texture_slots.add()
            TexSlot = Mat.texture_slots[0]
            TexSlot.texture = Tex
            TexSlot.use_map_alpha = True
            TexSlot.texture_coords = 'UV'
            if OpacityMode == 'TEX': TexSlot.alpha_factor = LayerOpacity
            elif OpacityMode == 'MAT': TexSlot.blend_type = 'MULTIPLY'
        
        else: # Ext == '.xjt'
            # Color JPG
            Tex = bpy.data.textures.new(NameShort, 'IMAGE')
            Tex.extension = 'CLIP'
            
            Img = bpy.data.images.new(NameShort)
            Img.source = 'FILE'
            Img.filepath = '%s%s%s' % (PathSaveRaw, Name, ExtSave)
            
            UVFace = Active.data.uv_textures[0].data[0]
            UVFace.image = Img
            UVFace.use_image = True
            
            Tex.image = Img
            
            Mat.texture_slots.add()
            TexSlot = Mat.texture_slots[0]
            TexSlot.texture = Tex
            TexSlot.texture_coords = 'UV'
            
            if HasAlpha:
                # Alpha JPG
                Tex = bpy.data.textures.new(NameShort+'_A', 'IMAGE')
                Tex.extension = 'CLIP'
                Tex.use_preview_alpha = True
                Tex.use_alpha = False
                
                Img = bpy.data.images.new(NameShort+'_A')
                Img.source = 'FILE'
                if PremulAlpha: Img.use_premultiply = True
                Img.filepath = '%s%s_A%s' % (PathSaveRaw, Name, ExtSave)
                
                Tex.image = Img
                
                Mat.texture_slots.add()
                TexSlot = Mat.texture_slots[1]
                TexSlot.texture = Tex
                TexSlot.use_map_alpha = True
                TexSlot.use_map_color_diffuse = False
                TexSlot.texture_coords = 'UV'
                if OpacityMode == 'TEX': TexSlot.alpha_factor = LayerOpacity
                elif OpacityMode == 'MAT': TexSlot.blend_type = 'MULTIPLY'
        
        if not Active.material_slots:
            bpy.ops.object.material_slot_add()
        
        Active.material_slots[0].material = Mat


    Z = 0
    global LayerNum
    LayerNum = 0
    LayerFlags = {}
    LayerList = []
    
    for Layer in IMGs:
        Make3DLayer(\
        Layer['LayerName'].replace(' ', '_'),\
        Layer['LayerNameShort'].replace(' ', '_'),\
        Z,\
        Layer['LayerCoords'],\
        Layer['RenderLayer'],\
        Layer['LayerMode'],\
        Layer['LayerOpacity'],\
        Layer['HasAlpha'],\
        )
        
        Z -= LayerOffset
    
    if SetupCompo:
        #-------------------------------------------------
        # COMPO NODES
        
        Scene.use_nodes = True
        
        Tree = Scene.node_tree
        
        for i in Tree.nodes:
            Tree.nodes.remove(i)
        
        LayerList.reverse()
        
        Offset = 0
        LayerLen = len(LayerList)
        
        for Layer in LayerList:
            
            Offset += 1
            
            X_Offset = (500*Offset)
            Y_Offset = (-300*Offset)
            
            Node = Tree.nodes.new('R_LAYERS')
            Node.location = (-500+X_Offset, 300+Y_Offset)
            Node.name = 'R_'+ str(Offset)
            Node.scene = Scene
            Node.layer = Layer[0]
            
            if LayerViewers:
                Node_V = Tree.nodes.new('VIEWER')
                Node_V.name = Layer[0]
                Node_V.location = (-200+X_Offset, 200+Y_Offset)
                
                Tree.links.new(Node.outputs[0], Node_V.inputs[0])
            
            if LayerLen > Offset:
                
                Mode = LayerList[Offset][1] # has to go one step further
                LayerOpacity = LayerList[Offset][2]
                
                if not Mode in ('Normal', '-1'):
                    
                    Node = Tree.nodes.new('MIX_RGB')
                    if OpacityMode == 'COMPO': Node.inputs['Fac'].default_value[0] = LayerOpacity
                    else: Node.inputs['Fac'].default_value[0] = 1
                    Node.use_alpha = True
                    
                    if Mode in ('Addition', '7'): Node.blend_type = 'ADD'
                    elif Mode in ('Subtract', '8'): Node.blend_type = 'SUBTRACT'
                    elif Mode in ('Multiply', '3'): Node.blend_type = 'MULTIPLY'
                    elif Mode in ('DarkenOnly', '9'): Node.blend_type = 'DARKEN'
                    elif Mode in ('Dodge', '16'): Node.blend_type = 'DODGE'
                    elif Mode in ('LightenOnly', '10'): Node.blend_type = 'LIGHTEN'
                    elif Mode in ('Difference', '6'): Node.blend_type = 'DIFFERENCE'
                    elif Mode in ('Divide', '15'): Node.blend_type = 'DIVIDE'
                    elif Mode in ('Overlay', '5'): Node.blend_type = 'OVERLAY'
                    elif Mode in ('Screen', '4'): Node.blend_type = 'SCREEN'
                    elif Mode in ('Burn', '17'): Node.blend_type = 'BURN'
                    elif Mode in ('Color', '13'): Node.blend_type = 'COLOR'
                    elif Mode in ('Value', '14'): Node.blend_type = 'VALUE'
                    elif Mode in ('Saturation', '12'): Node.blend_type = 'SATURATION'
                    elif Mode in ('Hue', '11'): Node.blend_type = 'HUE'
                    elif Mode in ('Softlight', '19'): Node.blend_type = 'SOFT_LIGHT'
                    else: pass
                    
                else:
                    Node = Tree.nodes.new('ALPHAOVER')
                    if OpacityMode == 'COMPO': Node.inputs['Fac'].default_value[0] = LayerOpacity
                Node.name = 'M_' + str(Offset)
                Node.location = (300+X_Offset, 250+Y_Offset)
                
                if MixerViewers:
                    Node_V = Tree.nodes.new('VIEWER')
                    Node_V.name = Layer[0]
                    Node_V.location = (500+X_Offset, 350+Y_Offset)
                    
                    Tree.links.new(Node.outputs[0], Node_V.inputs[0])
                
            else:
                Node = Tree.nodes.new('COMPOSITE')
                Node.name = 'Composite'
                Node.location = (400+X_Offset, 350+Y_Offset)
                
        Nodes = bpy.context.scene.node_tree.nodes
        
        if LayerLen > 1:
            for i in range (1, LayerLen+1):
                if i == 1:
                    Tree.links.new(Nodes['R_'+str(i)].outputs[0], Nodes['M_'+str(i)].inputs[1])
                if 1 < i < LayerLen:
                    Tree.links.new(Nodes['M_'+str(i-1)].outputs[0], Nodes['M_'+str(i)].inputs[1])
                if 1 < i < LayerLen+1:
                    Tree.links.new(Nodes['R_'+str(i)].outputs[0], Nodes['M_'+str(i-1)].inputs[2])
                if i == LayerLen:
                    Tree.links.new(Nodes['M_'+str(i-1)].outputs[0], Nodes['Composite'].inputs[0])
        else:
            Tree.links.new(Nodes['R_1'].outputs[0], Nodes['Composite'].inputs[0])
        
        for i in Tree.nodes:
            i.location[0] += -250*Offset
            i.location[1] += 150*Offset

#------------------------------------------------------------------------
import os
import bpy
from bpy.props import *
from math import pi

# Operator
class GIMPImageToScene(bpy.types.Operator):
    ''''''
    bl_idname = "import.gimp_image_to_scene"
    bl_label = "GIMP Image to Scene"
    bl_description = "Imports GIMP multilayer image files into 3D Scenes"
    bl_options = {'REGISTER', 'UNDO'}
    
    filename = StringProperty(name="File Name",
        description="Name of the file")
    directory = StringProperty(name="Directory",
        description="Directory of the file")
    
    LayerViewers = BoolProperty(name="Layer Viewers",
        description="Add Viewer nodes to each Render Layer node",
        default=True)
    
    MixerViewers = BoolProperty(name="Mixer Viewers",
        description="Add Viewer nodes to each Mix node",
        default=True)
    
    PremulAlpha = BoolProperty(name="Premuliply Alpha",
        description="Set Image and Render settings to premultiplied alpha",
        default=True)

    ShadelessMats = BoolProperty(name="Shadeless Material",
        description="Set Materials as Shadeless",
        default=True)
    
    OpacityMode = EnumProperty(name="Opacity Mode",
        description="Layer Opacity management",
        items=(
            ('TEX', 'Texture Alpha Factor', ''),
            ('MAT', 'Material Alpha Value', ''),
            ('COMPO', 'Mixer Node Factor', ''),
            ('BAKE', 'Baked in Image Alpha', '')),
        default='TEX')
    
    SetCamera = BoolProperty(name="Set Camera",
        description="Create an Ortho Camera matching image resolution",
        default=True)
        
    SetupCompo = BoolProperty(name="Setup Node Compositing",
        description="Create a compositing node setup (will delete existing nodes)",
        default=False)
    
    GroupUntagged = BoolProperty(name="Group Untagged",
        description="Layers with no tag go to a single Render Layer",
        default=False)
    
    LayerOffset = FloatProperty(name="Layer Separation",
        description="Distance between each 3D Layer in the Z axis",
        min=0,
        default=0.01)
    
    LayerScale = FloatProperty(name="Layer Scale",
        description="Scale pixel resolution by Blender units",
        min=0,
        default=0.01)
    
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label('3D Layers:', icon='SORTSIZE')
        box.prop(self, 'SetCamera', icon='OUTLINER_DATA_CAMERA')
        box.prop(self, 'OpacityMode', icon='GHOST')
        if self.OpacityMode == 'COMPO' and self.SetupCompo == False:
            box.label('Tip: Enable Node Compositing', icon='INFO')
        box.prop(self, 'PremulAlpha', icon='IMAGE_RGB_ALPHA')
        box.prop(self, 'ShadelessMats', icon='SOLID')
        box.prop(self, 'LayerOffset')
        box.prop(self, 'LayerScale')
        box = layout.box()
        box.label('Compositing:', icon='RENDERLAYERS')
        box.prop(self, 'SetupCompo', icon='NODETREE')
        if self.SetupCompo:
            box.prop(self, 'GroupUntagged', icon='IMAGE_ZDEPTH')
            box.prop(self, 'LayerViewers', icon='NODE')
            box.prop(self, 'MixerViewers', icon='NODE')
    
    def execute(self, context):
        # File Path
        filename = self.filename
        directory = self.directory
        
        # Settings
        LayerViewers = self.LayerViewers
        MixerViewers = self.MixerViewers
        OpacityMode = self.OpacityMode
        PremulAlpha = self.PremulAlpha
        ShadelessMats = self.ShadelessMats
        SetCamera = self.SetCamera
        SetupCompo = self.SetupCompo
        GroupUntagged = self.GroupUntagged
        LayerOffset = self.LayerOffset
        LayerScale = self.LayerScale
        
        Ext = None
        if filename.endswith('.xcf'): Ext = '.xcf'
        elif filename.endswith('.xjt'): Ext = '.xjt'
        
        # Call Main Function
        if Ext:
            main(filename, directory, LayerViewers, MixerViewers, LayerOffset,\
                LayerScale, OpacityMode, PremulAlpha, ShadelessMats,\
                SetCamera, SetupCompo, GroupUntagged, Ext)
        else:
            self.report({'ERROR'},"Selected file wasn't valid, try .xcf or .xjt")
        
        return {'FINISHED'}

    def invoke(self, context, event):
        wm = bpy.context.window_manager
        wm.fileselect_add(self)

        return {'RUNNING_MODAL'}


# Registering / Unregister
def menu_func(self, context):
    self.layout.operator(GIMPImageToScene.bl_idname, text="GIMP Image to Scene (.xcf, .xjt)", icon='PLUGIN')


def register():
    bpy.types.INFO_MT_file_import.append(menu_func)


def unregister():
    bpy.types.INFO_MT_file_import.remove(menu_func)


if __name__ == "__main__":
    register()