#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# GIMP plugin to save all layers in an image as separate files.

# (c) Ofnuts 2017-2025
#
#   History:
#
#   v0.00: 2017-12-12 First published version
#   v0.01: 2018-02-22 Change/add name/rawName formatting values
#   v0.02: 2023-01-08 Add support for group structure
#                    Use standard export dialog on first layer, reuse same settings on others
#   v0.03: 2023-07-14 Handle copied layers with autogenerated names ('image.ext  copy', 'image.ext copy#1'...)
#   v0.04: 2023-10-18 Fix default directory name
#   v0.05: 2023-10-24 Add offsetX/offsetY values in format
#   v1.00: 2025-03-29 Convert to Gimp3
#   v1.01: 2025-03-29 Downgrade Gio/Glib API
#   v1.02: 2025-04-01 Add top-level export option
#   v1.03: 2025-04-02 Detect cancellation by user from Gimp's file type export dialog
#   v1.04: 2025-04-04 Fix the sensitivy mask

#   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 3 of the License, or
#   (at your option) any later version.
#
#   This very file is the complete source code to the program.
#
#   If you make and redistribute changes to this code, please mark it
#   in reasonable ways as different from the original 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.
#
#   The GPL v3 licence is available at: https://www.gnu.org/licenses/gpl-3.0.en.html

import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
from gi.repository import GLib
from gi.repository import Gio
import sys, re
from gimphelpers import *

def _(text): return GLib.dgettext(None, text)

directoryArg=File('directory',_('Directory'),
                  _('Directory where files are saved'),
                  action=Gimp.FileChooserAction.CREATE_FOLDER,
                  noneOK=False)
namePatternArg=String('namePattern',_('Name pattern'),
                      _('Pattern to generate file names'),
                      _('{imageName}@{hierarchy}{name}.png'))
hierarchySepArg=String('hierarchySep', _('Hierarchy separator'),
                       _('Hierarchy separator'),
                       '-')
outputArg=Choice('output', _('Output'), _('How groups are saved'),'OUTPUT_D',
                 [
                     Option('OUTPUT_D', _('Directory hierarchy'),
                            blurb=_('A subdirectory is created for each group to hold the layers in the group')),
                     Option('OUTPUT_F', _('Single directory'),
                            blurb=_('All layers are exported to a single directory')),
                     Option('OUTPUT_T', _('Top-level only'),
                            blurb=_('All layers and groups are exported to a single directory, with top-level groups flattened to a single image')),
                 ]
                 )

def getLayers(gimpObject):
    match gimpObject:
        case gi.repository.Gimp.Image():
            return gimpObject.get_layers()
        case gi.repository.Gimp.GroupLayer():
            return gimpObject.get_children()
        case _:
            raise Exception(f'Unexpected type: {type(gimpObject)}')

def countLayersUnder(parent):
    count=0
    for l in getLayers(parent):
        if isinstance(l,Gimp.GroupLayer):
            count+=countLayersUnder(l)
        else:
            count+=1
    return count

def countLayersIn(parent):
    count=0
    for l in getLayers(parent):
        if not isinstance(l,Gimp.GroupLayer):
            count+=1
    return count

# Tries to nicely handle auto-numbered layers: 'Layer.jpg copy #1'
def simpleLayerName(fullName):
    name,ext=os.path.splitext(fullName)
    if ext:
        numberedCopy=re.match(r'.*( copy( \#\d+)?)$',ext)
        if numberedCopy:
            name+=numberedCopy.group(1)
    return name

def fileFromPathComponents(path:list[str]) -> Gio.File:
    return Gio.file_new_for_path(GLib.build_filenamev(path))

class LayerExporter(object):
    def __init__(self, image: Gimp.Image, directory: Gio.File, namePattern: str, hierarchySep: str, output: str):
        trace('Image: {:s}, directory: {:s}, namePattern: "{:s}", hierarchySep: "{:s}", hierarchySave: {:s}', image.get_name(), directory.get_path(), namePattern, hierarchySep, output)
        self.image=image
        self.directory=directory
        self.namePattern=namePattern
        self.hierarchySep=hierarchySep[:2] if hierarchySep else '-' 
        self.output=output
        self.layerImageIndex=0   # Global layer index
        self.layersImageCount=0
        self.imageValues={}

    
    def exportLayer(self,layer,hierarchy,groupValues,layerGroupIndex,layersGroupCount):
        layerValues={}
        layerValues['numDown0']=self.layerImageIndex
        layerValues['numDown1']=self.layerImageIndex+1
        layerValues['numUp0']=self.layersImageCount-(self.layerImageIndex+1)
        layerValues['numUp1']=self.layersImageCount-self.layerImageIndex
        layerValues['imageNumDown0']=self.layerImageIndex
        layerValues['imageNumDown1']=self.layerImageIndex+1
        layerValues['imageNumUp0']=self.layersImageCount-(self.layerImageIndex+1)
        layerValues['imageNumUp1']=self.layersImageCount-self.layerImageIndex
        layerValues['groupNumDown0']=layerGroupIndex
        layerValues['groupNumDown1']=layerGroupIndex+1
        layerValues['groupNumUp0']=layersGroupCount-(layerGroupIndex+1)
        layerValues['groupNumUp1']=layersGroupCount-layerGroupIndex
        layerValues['rawName']=layer.get_name()
        layerValues['name']=simpleLayerName(layer.get_name())
        layerValues['width']=layer.get_width()
        layerValues['height']=layer.get_height()
        layerValues['offsetX']=layer.get_offsets()[0]
        layerValues['offsetY']=layer.get_offsets()[1]

        trace("groupValues: {!r}, layerValues: {!r}", groupValues, layerValues)
        try:
            fileName=self.namePattern.format(**dict(groupValues,**layerValues))
        except KeyError as e:
            raise Exception('No formatting variable called "%s"' % e.args[0])
        if self.output=='OUTPUT_D':
            directory: Gio.File = fileFromPathComponents([self.directory.get_path()]+hierarchy)
        else:
            directory: Gio.File = self.directory
        dirType=directory.query_file_type(Gio.FileQueryInfoFlags.NONE)
        match dirType:
            case Gio.FileType.DIRECTORY: # Already there
                pass;
            case Gio.FileType.UNKNOWN: # Not already there
                directory.make_directory_with_parents(None)
            case _:
                raise Exception(f'{directory.get_path()} is not a directory')

        completeName: Gio.File = fileFromPathComponents([directory.get_path(),fileName])

        # Depending on RUN_INTERACTIVE/RUN_WITH_LAST_VALS Gimp will export a layer or a whole
        # image, so we play it safe by isolating the layer in its own image 
        exportImage=Gimp.Image.new(layer.get_width(),layer.get_height(),self.image.get_base_type())
        exportLayer=Gimp.Layer.new_from_drawable(layer, exportImage)
        exportLayer.set_offsets(0,0)
        exportImage.insert_layer(exportLayer,None,0)
        ok=Gimp.file_save(Gimp.RunMode.INTERACTIVE if self.layerImageIndex==0 else Gimp.RunMode.WITH_LAST_VALS,
            exportImage,completeName,None)
        exportImage.delete()
        if not ok:
            if self.layerImageIndex==0:
                raise Exception("Export not done, cancelled by user?")
            else:
                raise Exception("Error exporting layer, rest of export cancelled...")
        self.layerImageIndex+=1

    def exportLevel(self,parent,hierarchy): 
        trace("Parent: {!r}, hierarchy: {!r}", parent,hierarchy)
        groupValues=dict(self.imageValues)
        layersGroupCount=countLayersIn(parent)
        groupValues['groupCount']=layersGroupCount
        groupValues['parent']=hierarchy[-1] if hierarchy else '' 
        groupValues['hierarchy']=self.hierarchySep[0].join(hierarchy)+self.hierarchySep[-1] if hierarchy else ''
        trace("groupValues['hierarchy']={!r}",groupValues['hierarchy'])
        layerGroupIndex=0
        for layer in getLayers(parent):
            if isinstance(layer,Gimp.GroupLayer) and self.output!='OUTPUT_T':
                self.exportLevel(layer,hierarchy+[layer.get_name()])
            else: # Also applies to groups at top level if that option is used
                self.exportLayer(layer,hierarchy,groupValues,layerGroupIndex,layersGroupCount)
                layerGroupIndex+=1

    def export(self):
        rawImageName=self.image.get_name()
        if match:=re.match(r'\[(.+)\]$',rawImageName):
            imageName=match.group(1)
        else:
            imageName,_=os.path.splitext(rawImageName)
        self.imageValues['imageName']=imageName
        hierarchy=[]
        self.layersImageCount=countLayersUnder(self.image)
        self.imageValues['count']=self.layersImageCount
        self.imageValues['imageCount']=self.layersImageCount
        self.exportLevel(self.image,hierarchy)

def exportLayers(procedure: Gimp.ImageProcedure,
                 run_mode: Gimp.RunMode,
                 image: Gimp.Image,
                 drawables: list[Gimp.Drawable],
                 config: Gimp.ProcedureConfig, data: Any) -> Gimp.ValueArray:

    directory=directoryArg.getArgValue(config)
    namePattern=namePatternArg.getArgValue(config)
    hierarchySep=hierarchySepArg.getArgValue(config)
    output=outputArg.getArgValue(config)

    LayerExporter(image,directory,namePattern,hierarchySep,output).export()
    return successReturn(procedure)

class ExportLayers(HelpedPlugin):
    procedures=[
            ProcedureDescription(
                'ofn3-export-layers',
                '*', Gimp.ProcedureSensitivityMask.ALWAYS^Gimp.ProcedureSensitivityMask.NO_IMAGE,
                exportLayers,
                args=[
                    directoryArg,
                    outputArg,
                    namePatternArg,
                    hierarchySepArg,
                ],
                menuLabel=_('Export all layers'),
                menuPath=['<Image>/File/[Export]'],
                descShort=_('Export all layers'),
                descLong=_('Export all layers, using a pattern to generate the corresponding names'),
            )
    ]
    domain=getDomain(__file__)
    icon=getIcon(__file__)
    author='Ofnuts'
    year='2017-2024'

Gimp.main(ExportLayers.__gtype__, sys.argv)
