# ##### 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_info = {
    "name": "Import GIMP Image to Scene (.xcf/.xjt)",
    "author": "Daniel Salazar (ZanQdo)",
    "version": (2, 0, 0),
    "blender": (2, 5, 7),
    "api": 36079,
    "location": "File > Import > GIMP Image to Scene(.xcf/.xjt)",
    "description": "Imports GIMP multilayer image files as a series of multiple planes",
    "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 not bpy.data.is_saved:
        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"):
                
                '''The "nice" method to check if layer has alpha channel
                sadly GIMP sometimes decides not to export an alpha channel
                if it's pure white so we are not completly sure here yet'''
                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'
                        
                        '''Phisically double checking if alpha image exists
                        now we can be sure! (damn GIMP)'''
                        if HasAlpha:
                            if not os.path.isfile(PathSaveRaw+imageFileAlpha): HasAlpha = False
                        
                        # 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.spaces.active.viewport_shade = 'TEXTURED'
            Area.spaces.active.show_textured_solid = True
            Area.spaces.active.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.transform_apply(location=False, rotation=True, scale=False)

        
        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.transform_apply(location=False, rotation=False, scale=True)
        
        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, 128, 128)
            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
            
            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, 128, 128)
            Img.source = 'FILE'
            Img.filepath = '%s%s%s' % (PathSaveRaw, Name, ExtSave)
            
            UVFace = Active.data.uv_textures[0].data[0]
            UVFace.image = Img
            
            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', 128, 128)
                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.utils.register_module(__name__)

    bpy.types.INFO_MT_file_import.append(menu_func)


def unregister():
    bpy.utils.unregister_module(__name__)

    bpy.types.INFO_MT_file_import.remove(menu_func)


if __name__ == "__main__":
    register()
