Пример #1
0
class GimpDocumentationGenerator:
    def __init__(self, output: Output) -> None:
        super().__init__()
        self._output = output
        self._gsr = GimpScriptRunner()
        self._ordered_gimp_classes = []

    def __call__(self):
        print('starting install')
        self._document_pdb_module()
        self._output.start_classes()
        self._document_known_gimp_classes()
        self._document_unknown_gimp_classes()
        self._document_gimp_enums()
        self._document_gimpfu_constants()

    def _document_known_gimp_classes(self):
        gimp_classes = set([GIMP_TYPE_MAPPING[i] for i in KNOWN_GIMP_CLASSES])
        ordered_gimp_classes = self._get_ordered_gimp_classes()
        ordered_gimp_classes = [
            x for x in ordered_gimp_classes if x in gimp_classes
        ]
        for gimp_class in ordered_gimp_classes:
            attrs = self._execute(
                'from pgimp.gimp.parameter import return_json\n' +
                'attrs = filter(lambda s: not s.startswith("__"), dir(gimp.{0:s}))\n'
                .format(gimp_class) +
                'props = filter(lambda a: type(eval("gimp.{:s}." + a)).__name__ == "getset_descriptor", attrs)\n'
                .format(gimp_class) +
                'methods = filter(lambda a: type(eval("gimp.{:s}." + a)).__name__ == "method_descriptor", attrs)\n'
                .format(gimp_class) +
                'baseclasses = map(lambda cls: cls.__name__, gimp.{0:s}.__bases__)\n'
                .format(gimp_class) +
                'return_json({"props": props, "methods": methods, "baseclasses": baseclasses})'
            )
            props = attrs['props']
            methods = attrs['methods']
            baseclasses = attrs['baseclasses']
            self._output.start_class(gimp_class, baseclasses)
            self._output.class_properties(props)
            self._output.class_methods(methods)

    def _get_ordered_gimp_classes(self):
        if not self._ordered_gimp_classes:
            unordered_gimp_classes = self._execute(
                textwrap.dedent("""
                import gimp
                from pgimp.gimp.parameter import return_json
                import inspect
                classes = inspect.getmembers(gimp, inspect.isclass)
                return_json(map(lambda cls: (cls[0], inspect.getmro(cls[1])[1].__name__), classes))
                """))

            dependencies = {}
            for cls, parent in unordered_gimp_classes:
                if parent not in dependencies:
                    dependencies[parent] = []
                dependencies[parent].append(cls)
            visited = OrderedDict()
            to_visit = {'object'}
            while to_visit:
                to_visit_new = set([])
                for element in to_visit:
                    visited[element] = True
                    if element in dependencies:
                        to_visit_new = to_visit_new.union(
                            set(dependencies[element]))
                to_visit = to_visit_new - set(visited)

            self._ordered_gimp_classes = list(visited.keys())
        return self._ordered_gimp_classes

    def _document_unknown_gimp_classes(self):
        gimp_classes = [GIMP_TYPE_MAPPING[i] for i in UNKNOWN_GIMP_CLASSES]
        ordered_gimp_classes = self._get_ordered_gimp_classes()
        ordered_gimp_classes = [
            x for x in ordered_gimp_classes if x in gimp_classes
        ]
        for gimp_class in ordered_gimp_classes:
            self._output.start_unknown_class(gimp_class)

    def _document_pdb_module(self):
        self._output.start_module('pdb')
        pdb_dump = textwrap.dedent("""
            print("starting script")
            from collections import OrderedDict
            from pgimp.gimp.parameter import return_json

            result = OrderedDict()

            num_matches, procedure_names = pdb.gimp_procedural_db_query("", "", "", "", "", "", "")
            methods = sorted(procedure_names)
            print("iterating through methods")
            for method in methods:
                blurb, help, author, copyright, date, proc_type, num_args, num_values = pdb.gimp_procedural_db_proc_info(method)
                print(method)
                result[method] = OrderedDict()
                result[method]['blurb'] = blurb
                result[method]['help'] = help
                result[method]['args'] = OrderedDict()
                result[method]['vals'] = OrderedDict()
                for arg_num in range(0, num_args):
                    arg_type, arg_name, arg_desc = pdb.gimp_procedural_db_proc_arg(method, arg_num)
                    if arg_name == 'run-mode':
                        continue
                    result[method]['args'][arg_name] = OrderedDict()
                    result[method]['args'][arg_name]['type'] = arg_type
                    result[method]['args'][arg_name]['desc'] = arg_desc
                for val_num in range(0, num_values):
                    val_type, val_name, val_desc = pdb.gimp_procedural_db_proc_val(method, val_num)
                    result[method]['vals'][val_name] = OrderedDict()
                    result[method]['vals'][val_name]['type'] = val_type
                    result[method]['vals'][val_name]['desc'] = val_desc

            return_json(result)
            """)
        methods = self._execute(pdb_dump, 400)
        for method in methods.keys():
            blurb = methods[method]['blurb']
            help = methods[method]['help']

            description = ''
            if blurb:
                description += blurb + '\n'
            if blurb and help:
                description += '\n'
            if help:
                description += help

            parameters = OrderedDict()
            for arg_name in methods[method]['args'].keys():
                arg_type = methods[method]['args'][arg_name]['type']
                arg_desc = methods[method]['args'][arg_name]['desc']
                parameters[arg_name] = (GIMP_TYPE_MAPPING[arg_type], arg_desc
                                        or '')

            return_values = OrderedDict()
            for val_name in methods[method]['vals'].keys():
                val_type = methods[method]['vals'][val_name]['type']
                val_desc = methods[method]['vals'][val_name]['desc']
                return_values[val_name] = (GIMP_TYPE_MAPPING[val_type],
                                           val_desc or '')

            self._output.method(method, description, parameters, return_values)

    def _execute(self, string: str, timeout_in_seconds: int = 200):
        return self._gsr.execute_and_parse_json(
            string, timeout_in_seconds=timeout_in_seconds)

    def _document_gimp_enums(self):
        enum_dump = textwrap.dedent("""
            import gimpenums
            from pgimp.gimp.parameter import return_json

            result = filter(lambda s: not s.startswith('__'), dir(gimpenums))
            result = zip(result, map(lambda s: eval('gimpenums.' + s), result))
            result = filter(lambda v: type(v[1]).__name__ != 'instance', result)

            return_json(result)
            """)
        enums = self._execute(enum_dump)
        self._output.gimpenums(enums)

    def _document_gimpfu_constants(self):
        const_dump = textwrap.dedent("""
            import gimpfu
            from pgimp.gimp.parameter import return_json

            result = filter(lambda s: not s.startswith('__') and s.isupper(), dir(gimpfu))
            result = zip(result, map(lambda s: eval('gimpfu.' + s), result))
            result = filter(lambda v: type(v[1]).__name__ not in ['instance', 'function'] , result)

            return_json(result)
            """)
        constants = self._execute(const_dump)
        self._output.gimpfu_constants(constants)
Пример #2
0
class GimpFile:
    """
    Encapsulates functionality related to modifying gimp's xcf files and retreiving information from them.

    When interacting with numpy, please note that the array must be row-major (y,x-indexed).

    Example:

    >>> from pgimp.GimpFile import GimpFile
    >>> from pgimp.util.TempFile import TempFile
    >>> import numpy as np
    >>> with TempFile('.xcf') as f:
    ...     gimp_file = GimpFile(f).create('Background', np.zeros(shape=(32, 32), dtype=np.uint8))
    ...     gimp_file.layer_names()
    ['Background']
    """
    def __init__(
        self,
        file: str,
        short_running_timeout_in_seconds: int = 10,
        long_running_timeout_in_seconds: int = 20,
    ) -> None:
        super().__init__()
        self._file = file
        self._gsr = GimpScriptRunner()
        self.long_running_timeout_in_seconds = long_running_timeout_in_seconds
        self.short_running_timeout_in_seconds = short_running_timeout_in_seconds

    def get_file(self):
        """
        Returns the filename.

        :return: Filename.
        """
        return self._file

    def create(
        self,
        layer_name: str,
        layer_content: np.ndarray,
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Create a new gimp image with one layer from a numpy array.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as f:
        ...     gimp_file = GimpFile(f).create('Background', np.zeros(shape=(32, 32), dtype=np.uint8))
        ...     gimp_file.layer_names()
        ['Background']

        :param layer_name: Name of the layer to create.
        :param layer_content: Layer content, usually in the format of unsigned 8 bit integers.
        :param timeout: Execution timeout in seconds.
        :return: The newly created :py:class:`~pgimp.GimpFile.GimpFile`.
        """
        height, width, depth, image_type, layer_type = self._numpy_array_info(
            layer_content)

        tmpfile = tempfile.mktemp(suffix='.npy')
        np.save(tmpfile, layer_content)

        code = textwrap.dedent("""
            import gimp
            from pgimp.gimp.file import save_xcf
            from pgimp.gimp.layer import add_layer_from_numpy

            image = gimp.pdb.gimp_image_new({0:d}, {1:d}, {2:d})
            add_layer_from_numpy(image, '{6:s}', '{5:s}', image.width, image.height, {4:d})
            save_xcf(image, '{3:s}')
            """).format(width, height, image_type.value,
                        escape_single_quotes(self._file), layer_type,
                        escape_single_quotes(layer_name),
                        escape_single_quotes(tmpfile))

        self._gsr.execute(
            code,
            timeout_in_seconds=self.long_running_timeout_in_seconds
            if timeout is None else timeout)

        os.remove(tmpfile)
        return self

    def create_empty(
        self,
        width: int,
        height: int,
        type: GimpFileType = GimpFileType.RGB,
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Creates an empty image without any layers.

        Example:

        >>> from pgimp.GimpFile import GimpFile, GimpFileType
        >>> from pgimp.util.TempFile import TempFile
        >>> with TempFile('.xcf') as f:
        ...     gimp_file = GimpFile(f).create_empty(3, 2, GimpFileType.RGB)
        ...     gimp_file.layer_names()
        []

        :param width: Image width.
        :param height: Image height.
        :param type: Image type, e.g. rgb or gray.
        :param timeout: Execution timeout in seconds.
        :return: The newly created :py:class:`~pgimp.GimpFile.GimpFile`.
        """
        code = textwrap.dedent("""
            import gimp
            from pgimp.gimp.file import save_xcf
            image = gimp.pdb.gimp_image_new({0:d}, {1:d}, {2:d})
            save_xcf(image, '{3:s}')
            """).format(width, height, type.value,
                        escape_single_quotes(self._file))

        self._gsr.execute(
            code,
            timeout_in_seconds=self.short_running_timeout_in_seconds
            if timeout is None else timeout)
        return self

    def create_indexed(
        self,
        layer_name: str,
        layer_content: np.ndarray,
        colormap: Union[np.ndarray, ColorMap],
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Create a new indexed gimp image with one layer from a numpy array. An indexed image has a single channel
        and displays the values using a colormap.

        Example using a predefined colormap:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> from pgimp.GimpFile import ColorMap
        >>> import numpy as np
        >>> with TempFile('.xcf') as f:
        ...     gimp_file = GimpFile(f).create_indexed(
        ...         'Background',
        ...         np.arange(0, 256, dtype=np.uint8).reshape((1, 256)),
        ...         ColorMap.JET
        ...     )

        Example using a custom colormap:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> from pgimp.GimpFile import ColorMap
        >>> import numpy as np
        >>> with TempFile('.xcf') as f:
        ...     gimp_file = GimpFile(f).create_indexed(
        ...         'Background',
        ...         np.arange(0, 256, dtype=np.uint8).reshape((1, 256)),
        ...         np.array(
        ...             [[255, 0, 0], [0, 255, 0], [0, 0, 255], *[[i, i, i] for i in range(3, 256)]],
        ...             dtype=np.uint8
        ...         )
        ...     )

        :param layer_name: Name of the layer to create.
        :param layer_content: Layer content, usually in the format of unsigned 8 bit integers.
        :param timeout: Execution timeout in seconds.
        :return: The newly created :py:class:`~pgimp.GimpFile.GimpFile`.
        """
        if isinstance(colormap, np.ndarray):
            if not len(layer_content.shape) == 2 and not (len(
                    layer_content.shape) == 3 and layer_content.shape[2] == 1):
                raise DataFormatException(
                    'Indexed images can only contain one channel')
            colormap = 'np.frombuffer({0}, dtype=np.uint8).reshape((256, 3))'.format(
                colormap.tobytes())
        if isinstance(colormap, ColorMap):
            colormap = colormap.value

        tmpfile = tempfile.mktemp(suffix='.npy')
        np.save(tmpfile, layer_content)

        code = textwrap.dedent("""
            import gimp
            import gimpenums
            from pgimp.gimp.file import save_xcf
            from pgimp.gimp.colormap import *  # necessary for predefined colormaps
            from pgimp.gimp.layer import add_layer_from_numpy

            cmap = {0:s}
            image = gimp.pdb.gimp_image_new({1:d}, {2:d}, gimpenums.GRAY)
            palette_name = gimp.pdb.gimp_palette_new('colormap')
            for i in range(0, cmap.shape[0]):
                gimp.pdb.gimp_palette_add_entry(palette_name, str(i), (int(cmap[i][0]), int(cmap[i][1]), int(cmap[i][2])))
            gimp.pdb.gimp_convert_indexed(image, gimpenums.NO_DITHER, gimpenums.CUSTOM_PALETTE, 256, False, False, palette_name)

            add_layer_from_numpy(image, '{5:s}', '{4:s}', image.width, image.height, gimpenums.INDEXED_IMAGE)
            save_xcf(image, '{3:s}')
            """).format(colormap,
                        layer_content.shape[1], layer_content.shape[0],
                        escape_single_quotes(self._file),
                        escape_single_quotes(layer_name),
                        escape_single_quotes(tmpfile))

        self._gsr.execute(
            code,
            timeout_in_seconds=self.long_running_timeout_in_seconds
            if timeout is None else timeout)

        os.remove(tmpfile)
        return self

    def create_from_template(
        self,
        other_file: 'GimpFile',
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Create a new gimp file without any layers from a template containing the dimensions (width, height)
        and the image type.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> with TempFile('.xcf') as original, TempFile('.xcf') as created:
        ...     original_file = GimpFile(original).create('Background', np.zeros(shape=(3, 2), dtype=np.uint8))
        ...     created_file = GimpFile(created).create_from_template(original_file)
        ...     created_file.layer_names()
        []

        :param other_file: The template file.
        :param timeout: Execution timeout in seconds.
        :return: The newly created :py:class:`~pgimp.GimpFile.GimpFile`.
        """
        code = textwrap.dedent("""
            from pgimp.gimp.file import save_xcf
            from pgimp.gimp.image import create_from_template_file
            image = create_from_template_file('{0:s}')
            save_xcf(image, '{1:s}')
            """).format(escape_single_quotes(other_file._file),
                        escape_single_quotes(self._file))

        self._gsr.execute(
            code,
            timeout_in_seconds=self.short_running_timeout_in_seconds
            if timeout is None else timeout)
        return self

    def create_from_file(
        self,
        file: str,
        layer_name: str = 'Background',
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Create a new gimp file by importing an image from another format.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as xcf, TempFile('.png') as png, TempFile('.xcf') as from_png:
        ...     gimp_file = GimpFile(xcf) \\
        ...         .create('Background', np.zeros(shape=(1, 1), dtype=np.uint8)) \\
        ...         .add_layer_from_numpy('Foreground', np.ones(shape=(1, 1), dtype=np.uint8)*255, opacity=50.) \\
        ...         .export(png)  # saved as grayscale with alpha (identify -format '%[channels]' FILE)
        ...     GimpFile(from_png).create_from_file(png, layer_name='Image').layer_to_numpy('Image')
        array([[[127, 255]]], dtype=uint8)

        :param file: File to import into gimp.
        :param layer_name: The layer name for the data to be imported.
        :param timeout: Execution timeout in seconds.
        :return:
        """
        code = textwrap.dedent("""
            from pgimp.gimp.file import save_xcf
            from pgimp.gimp.image import create_from_file
            image = create_from_file('{0:s}')
            image.layers[0].name = '{2:s}'
            save_xcf(image, '{1:s}')
            """).format(escape_single_quotes(file),
                        escape_single_quotes(self._file),
                        escape_single_quotes(layer_name))

        self._gsr.execute(
            code,
            timeout_in_seconds=self.short_running_timeout_in_seconds
            if timeout is None else timeout)
        return self

    def copy(
        self,
        filename: str,
    ) -> 'GimpFile':
        """
        Copies a gimp file.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> with TempFile('.xcf') as original, TempFile('.xcf') as copy:
        ...     original_file = GimpFile(original).create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     copied_file = original_file.copy(copy)
        ...     copied_file.layer_names()
        ['Background']

        :param filename: Destination filename relative to the source filename. Or an absolute path.
        :return: The copied exemplar of :py:class:`~pgimp.GimpFile.GimpFile`.
        """
        dst = file.copy_relative(self._file, filename)
        return GimpFile(dst)

    def layer_to_numpy(
        self,
        layer_name: str,
        timeout: Optional[int] = None,
    ) -> np.ndarray:
        """
        Convert a gimp layer to a numpy array of unsigned 8 bit integers.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as f:
        ...     gimp_file = GimpFile(f).create('Background', np.zeros(shape=(1, 2, 1), dtype=np.uint8))
        ...     gimp_file.layer_to_numpy('Background').shape
        (1, 2, 1)

        :param layer_name: Name of the layer to convert.
        :param timeout: Execution timeout in seconds.
        :return: Numpy array of unsigned 8 bit integers.
        """
        return self.layers_to_numpy(
            [layer_name],
            timeout=self.long_running_timeout_in_seconds
            if timeout is None else timeout)

    def layers_to_numpy(
        self,
        layer_names: List[str],
        use_temp_file=True,
        timeout: Optional[int] = None,
    ) -> np.ndarray:
        """
        Convert gimp layers to a numpy array of unsigned 8 bit integers.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as f:
        ...     gimp_file = GimpFile(f) \\
        ...         .create('Red', np.zeros(shape=(1, 2, 1), dtype=np.uint8)) \\
        ...         .add_layer_from_numpy('Green', np.ones(shape=(1, 2, 1), dtype=np.uint8)*127) \\
        ...         .add_layer_from_numpy('Blue', np.ones(shape=(1, 2, 1), dtype=np.uint8)*255)
        ...     gimp_file.layers_to_numpy(['Red', 'Green', 'Blue']).shape
        (1, 2, 3)

        :param layer_names: Names of the layers to convert.
        :param use_temp_file: Use a tempfile for data transmition instead of stdout. This is more robust in
                              a multiprocessing setting.
        :param timeout: Execution timeout in seconds.
        :return: Numpy array of unsigned 8 bit integers.
        """
        with TempFile('.npy') as tmpfile:
            bytes = self._gsr.execute_binary(
                textwrap.dedent(
                    """
                    import numpy as np
                    import sys
                    from pgimp.gimp.file import open_xcf
                    from pgimp.gimp.parameter import get_json, get_string
                    from pgimp.gimp.layer import convert_layers_to_numpy
    
                    np_buffer = convert_layers_to_numpy(open_xcf('{0:s}'), get_json('layer_names', '[]'))
                    temp_file = get_string('temp_file')
                    if temp_file:
                        np.save(temp_file, np_buffer)
                    else:
                        np.save(sys.stdout, np_buffer)
                    """, ).format(escape_single_quotes(self._file)),
                parameters={
                    'layer_names': layer_names,
                    'temp_file': tmpfile if use_temp_file else ''
                },
                timeout_in_seconds=self.long_running_timeout_in_seconds
                if timeout is None else timeout)
            if use_temp_file:
                return np.load(tmpfile)

        return np.load(io.BytesIO(bytes))

    def add_layer_from_numpy(
        self,
        layer_name: str,
        layer_content: np.ndarray,
        opacity: float = 100.0,
        visible: bool = True,
        position: Union[int, str] = 0,
        type: LayerType = None,
        blend_mode: Union[int, List[int]] = gimpenums.NORMAL_MODE,
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Adds a new layer to the gimp file from numpy data, usually as unsigned 8 bit integers.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as f:  # doctest:+ELLIPSIS
        ...     gimp_file = GimpFile(f).create('Background', np.zeros(shape=(1, 2), dtype=np.uint8))
        ...     gimp_file.add_layer_from_numpy('Foreground', np.ones(shape=(1, 2), dtype=np.uint8)*255, opacity=55., visible=False)
        ...     gimp_file.layer_names()
        <...>
        ['Foreground', 'Background']

        :param layer_name: Name of the layer to add.
        :param layer_content: Layer content, usually as unsigned 8 bit integers.
        :param opacity: How transparent the layer should be (opacity is the inverse of transparency).
        :param visible: Whether the layer should be visible.
        :param position: Position in the stack of layers. On top = 0, bottom = number of layers.
            In case a layer name is specified, the new layer will be added on top of the layer with the given name.
        :param type: Layer type. Indexed images should use indexed layers.
        :param blend_mode: Affects the display of the current layer. Blend mode normal means no blending.
        :param timeout: Execution timeout in seconds.
        :return: :py:class:`~pgimp.GimpFile.GimpFile`
        """
        return self.add_layers_from_numpy([layer_name],
                                          np.expand_dims(layer_content,
                                                         axis=0), opacity,
                                          visible, position, type, blend_mode,
                                          timeout)

    def add_layers_from_numpy(
        self,
        layer_names: List[str],
        layer_contents: np.ndarray,
        opacity: Union[float, List[float]] = 100.0,
        visible: Union[bool, List[bool]] = True,
        position: Union[int, str] = 0,
        type: LayerType = None,
        blend_mode: Union[int, List[int]] = gimpenums.NORMAL_MODE,
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Adds new layers to the gimp file from numpy data, usually as unsigned 8 bit integers.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as f:  # doctest:+ELLIPSIS
        ...     gimp_file = GimpFile(f).create('Background', np.zeros(shape=(1, 2), dtype=np.uint8))
        ...     gimp_file.add_layers_from_numpy(['Layer 1', 'Layer 2'], np.ones(shape=(2, 1, 2), dtype=np.uint8)*255, opacity=55., visible=False, position='Background')
        ...     gimp_file.layer_names()
        <...>
        ['Layer 1', 'Layer 2', 'Background']

        :param layer_names: Names of the layers to add.
        :param layer_contents: Layer content, usually as unsigned 8 bit integers. First axis indexes the layer.
        :param opacity: How transparent the layer should be (opacity is the inverse of transparency).
        :param visible: Whether the layer should be visible.
        :param position: Position in the stack of layers. On top = 0, bottom = number of layers.
            In case a layer name is specified, the new layers will be added on top of the layer with the given name.
        :param type: Layer type. Indexed images should use indexed layers.
        :param blend_mode: Affects the display of the current layer. Blend mode normal means no blending.
        :param timeout: Execution timeout in seconds.
        :return: :py:class:`~pgimp.GimpFile.GimpFile`
        """
        if len(layer_contents) == 0:
            raise ValueError('Layer contents must not be empty')
        if len(layer_contents) != len(layer_names):
            raise ValueError('Layer contents must exist for each layer name.')

        height, width, depth, image_type, layer_type = self._numpy_array_info(
            layer_contents[0])
        if type is not None:
            layer_type = type.value

        tmpfile = tempfile.mktemp(suffix='.npy')
        np.save(tmpfile, layer_contents)

        code = textwrap.dedent("""
            from pgimp.gimp.file import XcfFile
            from pgimp.gimp.layer import add_layers_from_numpy
            from pgimp.gimp.parameter import get_json, get_int, get_string
            
            with XcfFile(get_string('file'), save=True) as image:
                position = get_json('position')[0]
                add_layers_from_numpy(
                    image, get_string('tmpfile'), 
                    get_json('layer_names'), 
                    get_int('width'), 
                    get_int('height'), 
                    get_int('layer_type'), 
                    position, 
                    get_json('opacity')[0],
                    get_json('blend_mode')[0], 
                    get_json('visible')[0]
                )
            """)

        self._gsr.execute(
            code,
            parameters={
                'width': width,
                'height': height,
                'file': self._file,
                'layer_type': layer_type,
                'layer_names': layer_names,
                'tmpfile': tmpfile,
                'visible': [visible],
                'opacity': [opacity],
                'position': [position],
                'blend_mode': [blend_mode],
            },
            timeout_in_seconds=self.long_running_timeout_in_seconds
            if timeout is None else timeout)

        os.remove(tmpfile)
        return self

    def _numpy_array_info(self, content: np.ndarray):
        if content.dtype != np.uint8:
            raise DataFormatException('Only uint8 is supported')

        if len(content.shape) == 2:
            height, width = content.shape
            depth = 1
        elif len(content.shape) == 3 and content.shape[2] in [1, 3]:
            height, width, depth = content.shape
        else:
            raise DataFormatException('Unrecognized input data shape: ' +
                                      repr(content.shape))

        if depth == 1:
            image_type = GimpFileType.GRAY
        elif depth == 3:
            image_type = GimpFileType.RGB
        else:
            raise DataFormatException('Wrong image depth {:d}'.format(depth))

        layer_type = image_type_to_layer_type[image_type]

        return height, width, depth, image_type, layer_type

    def add_layer_from_file(
        self,
        other_file: 'GimpFile',
        name: str,
        new_name: str = None,
        new_type: GimpFileType = GimpFileType.RGB,
        new_position: int = 0,
        new_visibility: Optional[bool] = None,
        new_opacity: Optional[float] = None,
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Adds a new layer to the gimp file from another gimp file.

        Example:

        >>> from pgimp.GimpFile import GimpFile, GimpFileType
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as other, TempFile('.xcf') as current:  # doctest:+ELLIPSIS
        ...     green_content = np.zeros(shape=(1, 1, 3), dtype=np.uint8)
        ...     green_content[:, :] = [0, 255, 0]
        ...     other_file = GimpFile(other).create('Green', green_content)
        ...     current_file = GimpFile(current).create('Background', np.zeros(shape=(1, 1, 3), dtype=np.uint8))
        ...     current_file.add_layer_from_file(
        ...         other_file,
        ...         'Green',
        ...         new_name='Green (copied)',
        ...         new_type=GimpFileType.RGB, new_position=1
        ...     )
        ...     current_file.layer_names()
        ...     current_file.layer_to_numpy('Green (copied)')
        <...>
        ['Background', 'Green (copied)']
        array([[[  0, 255,   0]]], dtype=uint8)

        :param other_file: The gimp file from which to copy the layer into the current image.
        :param name: The layer name in the other file to copy over to the current file. Also the layer name
                     in the current file if no new name is set.
        :param new_name: The new layer name in the current image. Same as the layer name in the other file if not set.
        :param new_type: The layer type to create in the current image. E.g. rgb or grayscale.
        :param new_position: Position in the stack of layers. On top = 0, bottom = number of layers.
        :param new_visibility: Visibility of the layer if it should be changed.
        :param new_opacity: Opacity for the layer if it should be changed.
        :param timeout: Execution timeout in seconds.
        :return: :py:class:`~pgimp.GimpFile.GimpFile`
        """
        code = textwrap.dedent("""
            from pgimp.gimp.parameter import get_json
            from pgimp.gimp.file import XcfFile
            from pgimp.gimp.layer import copy_layer
            
            params = get_json('params')
            new_position = params['new_position']
            new_visibility = params['new_visibility']
            new_opacity = params['new_opacity']

            with XcfFile('{1:s}') as image_src, XcfFile('{0:s}', save=True) as image_dst:
                copy_layer(image_src, '{3:s}', image_dst, '{2:s}', new_position)
                if new_visibility is not None:
                    image_dst.layers[new_position].visible = new_visibility
                if new_opacity is not None:
                    image_dst.layers[new_position].opacity = float(new_opacity)
            """).format(
            escape_single_quotes(self._file),
            escape_single_quotes(other_file._file),
            escape_single_quotes(new_name or name),
            escape_single_quotes(name),
            new_type.value,
        )

        self._gsr.execute(
            code,
            timeout_in_seconds=self.long_running_timeout_in_seconds
            if timeout is None else timeout,
            parameters={
                'params': {
                    'new_visibility': new_visibility,
                    'new_position': new_position,
                    'new_opacity': new_opacity,
                }
            })
        return self

    def merge_layer_from_file(
        self,
        other_file: 'GimpFile',
        name: str,
        clear_selection: bool = True,
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Merges a layer from another file into the current file. The layer must exist in the current file.

        Example:

        >>> from pgimp.GimpFile import GimpFile, GimpFileType
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as other, TempFile('.xcf') as current:  # doctest:+ELLIPSIS
        ...     green_content = np.zeros(shape=(1, 1, 3), dtype=np.uint8)
        ...     green_content[:, :] = [0, 255, 0]
        ...     other_file = GimpFile(other).create('Green', green_content)
        ...     current_file = GimpFile(current).create('Green', np.zeros(shape=(1, 1, 3), dtype=np.uint8))
        ...     current_file.merge_layer_from_file(other_file, 'Green')
        ...     current_file.layer_names()
        ...     current_file.layer_to_numpy('Green')
        <...>
        ['Green']
        array([[[  0, 255,   0]]], dtype=uint8)

        :param other_file: The gimp file from which the layer contents are merged into the current file.
        :param name: Name of the layer to merge.
        :param clear_selection: Clear selection before merging to avoid only merging the selection.
        :param timeout: Execution timeout in seconds.
        :return: :py:class:`~pgimp.GimpFile.GimpFile`
        """
        code = textwrap.dedent("""
            from pgimp.gimp.file import XcfFile
            from pgimp.gimp.layer import merge_layer

            with XcfFile('{1:s}') as image_src, XcfFile('{0:s}', save=True) as image_dst:
                merge_layer(image_src, '{2:s}', image_dst, '{2:s}', 0, {3:s})
            """).format(escape_single_quotes(self._file),
                        escape_single_quotes(other_file._file),
                        escape_single_quotes(name), str(clear_selection))

        self._gsr.execute(
            code,
            timeout_in_seconds=self.long_running_timeout_in_seconds
            if timeout is None else timeout)
        return self

    def layers(
        self,
        timeout: Optional[int] = None,
    ) -> List[Layer]:
        """
        Returns the image layers. The topmost layer is the first element, the bottommost the last element.
        
        :param timeout: Execution timeout in seconds.
        :return: List of :py:class:`~pgimp.layers.Layer`.
        """
        code = textwrap.dedent("""
            from pgimp.gimp.file import open_xcf
            from pgimp.gimp.parameter import return_json

            image = open_xcf('{0:s}')

            result = []
            for layer in image.layers:
                properties = dict()
                properties['name'] = layer.name
                properties['visible'] = layer.visible
                properties['opacity'] = layer.opacity
                result.append(properties)

            return_json(result)
            """.format(escape_single_quotes(self._file)))

        result = self._gsr.execute_and_parse_json(
            code,
            timeout_in_seconds=self.short_running_timeout_in_seconds
            if timeout is None else timeout)
        layers = []
        for idx, layer_properties in enumerate(result):
            layer_properties['position'] = idx
            layers.append(Layer(layer_properties))

        return layers

    def layer_names(
        self,
        timeout: Optional[int] = None,
    ) -> List[str]:
        """
        Returns the names of the layers in the gimp file. The topmost layer is the first element,
        the bottommost the last element.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as file:
        ...     gimp_file = GimpFile(file) \\
        ...         .create('Background', np.zeros(shape=(2, 2), dtype=np.uint8)) \\
        ...         .add_layer_from_numpy('Black', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gimp_file.layer_names()
        ['Black', 'Background']
        
        :param timeout: Execution timeout in seconds.
        :return: List of layer names.
        """
        return list(
            map(
                lambda l: l.name,
                self.layers(timeout=self.short_running_timeout_in_seconds
                            if timeout is None else timeout)))

    def remove_layer(
        self,
        layer_name: str,
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Removes a layer from the gimp file.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as file:
        ...     gimp_file = GimpFile(file) \\
        ...         .create('Background', np.zeros(shape=(2, 2), dtype=np.uint8)) \\
        ...         .add_layer_from_numpy('Black', np.zeros(shape=(2, 2), dtype=np.uint8)) \\
        ...         .remove_layer('Background')
        ...     gimp_file.layer_names()
        ['Black']

        :param layer_name: Name of the layer to remove.
        :param timeout: Execution timeout in seconds.
        :return: :py:class:`~pgimp.GimpFile.GimpFile`
        """
        code = textwrap.dedent("""
            from pgimp.gimp.file import XcfFile
            from pgimp.gimp.layer import remove_layer

            with XcfFile('{0:s}', save=True) as image:
                remove_layer(image, '{1:s}')
            """).format(escape_single_quotes(self._file),
                        escape_single_quotes(layer_name))

        self._gsr.execute(
            code,
            timeout_in_seconds=self.short_running_timeout_in_seconds
            if timeout is None else timeout)
        return self

    def dimensions(
        self,
        timeout: Optional[int] = None,
    ) -> Tuple[int, int]:
        """
        Return the image dimensions (width, height).

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> with TempFile('.xcf') as f:
        ...     gimp_file = GimpFile(f).create('Background', np.zeros(shape=(3, 2), dtype=np.uint8))
        ...     gimp_file.dimensions()
        (2, 3)

        :param timeout: Execution timeout in seconds.
        :return: Tuple of width and height.
        """
        code = textwrap.dedent("""
            from pgimp.gimp.file import open_xcf
            from pgimp.gimp.parameter import return_json

            image = open_xcf('{0:s}')
            return_json([image.width, image.height])
            """).format(escape_single_quotes(self._file))

        dimensions = self._gsr.execute_and_parse_json(
            code,
            timeout_in_seconds=self.short_running_timeout_in_seconds
            if timeout is None else timeout)
        return tuple(dimensions)

    def export(
        self,
        file: str,
        timeout: Optional[int] = None,
    ) -> 'GimpFile':
        """
        Export a gimp file to another file format based on the file extension.

        Gimp will apply defaults for encoding to the desired format. E.g. png is saved including an alpha channel
        and jpg has no alpha channel but will use default compression settings.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('.xcf') as xcf, TempFile('.png') as png, TempFile('.xcf') as from_png:
        ...     gimp_file = GimpFile(xcf) \\
        ...         .create('Background', np.zeros(shape=(1, 1), dtype=np.uint8)) \\
        ...         .add_layer_from_numpy('Foreground', np.ones(shape=(1, 1), dtype=np.uint8)*255, opacity=50.) \\
        ...         .export(png)  # saved as grayscale with alpha (identify -format '%[channels]' FILE)
        ...     GimpFile(from_png).create_from_file(png, layer_name='Image').layer_to_numpy('Image')
        array([[[127, 255]]], dtype=uint8)

        :param file: Filename including the desired extension to export to.
        :param timeout: Execution timeout in seconds.
        :return: :py:class:`~pgimp.GimpFile.GimpFile`
        """

        code = textwrap.dedent("""
            import gimp
            import gimpenums
            from pgimp.gimp.file import XcfFile
            with XcfFile('{0:s}') as image:
                merged = gimp.pdb.gimp_image_merge_visible_layers(image, gimpenums.CLIP_TO_IMAGE)
                gimp.pdb.gimp_file_save(image, merged, '{1:s}', '{1:s}')
            """).format(escape_single_quotes(self._file),
                        escape_single_quotes(file))

        self._gsr.execute(
            code,
            timeout_in_seconds=self.short_running_timeout_in_seconds
            if timeout is None else timeout)
        return self
Пример #3
0
class GimpFileCollection:
    def __init__(self, files: List[str], gimp_file_factory=lambda file: GimpFile(file)) -> None:
        super().__init__()
        self._files = files
        self._gimp_file_factory = gimp_file_factory
        self._gsr = GimpScriptRunner()

    def get_files(self) -> List[str]:
        """
        Returns the list of files contained in the collection.

        Example:

        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> gfc = GimpFileCollection(['a.xcf', 'b.xcf'])
        >>> gfc.get_files()
        ['a.xcf', 'b.xcf']

        :return: List of files contained in the collection
        """
        return self._files

    def get_prefix(self) -> str:
        """
        Returns the common path prefix for all files including a trailing slash.

        Example:

        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> gfc = GimpFileCollection(['common/pre/dir/a.xcf', 'common/pre/files/b.xcf'])
        >>> gfc.get_prefix()
        'common/pre/'

        :return: Common path prefix for all files including a trailing slash.
        """
        if not self._files:
            return ''
        if len(self._files) == 1:
            return os.path.dirname(self._files[0]) + '/'
        commonprefix = os.path.commonprefix(self._files)
        if os.path.isdir(commonprefix):
            return commonprefix
        return os.path.dirname(commonprefix) + '/'

    def replace_prefix(self, prefix: str, new_prefix: str = '') -> 'GimpFileCollection':
        """
        Returns a new collection with filenames where the old prefix is replaced by a new prefix.

        Example:

        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> gfc = GimpFileCollection(['common/pre/dir/a.xcf', 'common/pre/files/b.xcf'])
        >>> gfc.replace_prefix('common/pre', 'newpre').get_files()
        ['newpre/dir/a.xcf', 'newpre/files/b.xcf']

        :param prefix: The prefix to strip away.
        :param new_prefix: The replacement value for the prefix.
        :return: A :py:class:`~pgimp.GimpFileCollection.GimpFileCollection` where file prefixes are stripped away.
        """
        return self.replace_path_components(prefix=prefix, new_prefix=new_prefix)

    def replace_suffix(self, suffix: str, new_suffix: str = '') -> 'GimpFileCollection':
        """
        Returns a new collection with filenames that do not contain the suffix.

        Example:

        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> gfc = GimpFileCollection(['dir/a_tmp.xcf', 'files/b_tmp.xcf'])
        >>> gfc.replace_suffix('tmp', 'final').get_files()
        ['dir/a_final.xcf', 'files/b_final.xcf']

        :param suffix: The suffix to strip away.
        :param new_suffix: The replacement value for the suffix.
        :return: A :py:class:`~pgimp.GimpFileCollection.GimpFileCollection` where file suffixed are stripped away.
        """
        return self.replace_path_components(suffix=suffix, new_suffix=new_suffix)

    def replace_path_components(
            self,
            prefix: str = '',
            new_prefix: str = '',
            suffix: str = '',
            new_suffix: str = ''
    ) -> 'GimpFileCollection':
        """
        Returns a new collection with replaced path components.

        Example:

        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> gfc = GimpFileCollection(['pre/filepre_a_suf.xcf', 'pre/filepre_b_suf.xcf'])
        >>> gfc.replace_path_components(prefix='pre/filepre_', new_prefix='', suffix='_suf', new_suffix='').get_files()
        ['a.xcf', 'b.xcf']

        :param prefix: The prefix to replace.
        :param suffix: The suffix to replace.
        :param new_prefix: The replacement value for the prefix.
        :param new_suffix: The replacement value for the suffix.
        :return: A :py:class:`~pgimp.GimpFileCollection.GimpFileCollection` where the given path components
                 are replaced.
        """
        files = self._files
        if not suffix.endswith(EXTENSION):
            suffix += EXTENSION
        if not new_suffix.endswith(EXTENSION):
            new_suffix += EXTENSION
        check = list(filter(lambda file: file.startswith(prefix) and file.endswith(suffix), files))
        if len(check) != len(files):
            raise NonExistingPathComponentException(
                'All files must start with the given prefix and end with the given suffix.'
            )

        prefix_length = len(prefix)
        files = list(map(lambda file: new_prefix + file[prefix_length:], files))

        suffix_length = len(suffix)
        files = map(lambda file: file[:-suffix_length] + new_suffix, files)
        return GimpFileCollection(list(files))

    def find_files_containing_layer_by_predictate(self, predicate: Callable[[List[Layer]], bool]) -> List[str]:
        """
        Find files that contain a layer matching the predicate.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('_bg.xcf') as f1, TempFile('_fg.xcf') as f2:  # doctest: +ELLIPSIS
        ...     gf1 = GimpFile(f1).create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gf2 = GimpFile(f2).create('Foreground', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gfc = GimpFileCollection.create_from_gimp_files([gf1, gf2])
        ...     def find_foreground(layers: List[Layer]):
        ...         return list(filter(lambda layer: layer.name == 'Foreground', layers)) != []
        ...     gfc.find_files_containing_layer_by_predictate(find_foreground)
        ['..._fg.xcf']

        :param predicate: A function that takes a list of layers and returns bool.
        :return: List of files matching the predicate.
        """
        return list(filter(lambda file: predicate(self._gimp_file_factory(file).layers()), self._files))

    def find_files_containing_layer_by_name(self, layer_name: str, timeout_in_seconds: float = None) -> List[str]:
        """
        Find files that contain a layer that matching the given name.

        Example:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.util.TempFile import TempFile
        >>> import numpy as np
        >>> with TempFile('_bg.xcf') as f1, TempFile('_fg.xcf') as f2:  # doctest: +ELLIPSIS
        ...     gf1 = GimpFile(f1).create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gf2 = GimpFile(f2).create('Foreground', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gfc = GimpFileCollection.create_from_gimp_files([gf1, gf2])
        ...     gfc.find_files_containing_layer_by_name('Foreground')
        ['..._fg.xcf']

        :param layer_name: Layer name to search for.
        :param timeout_in_seconds: Script execution timeout in seconds.
        :return: List of files containing the layer with the given name.
        """
        return self.find_files_by_script(textwrap.dedent(
            """
            from pgimp.gimp.parameter import return_json, get_json
            from pgimp.gimp.file import XcfFile
            files = get_json('__files__')
            matches = []
            for file in files:
                with XcfFile(file) as image:
                    for layer in image.layers:
                        if layer.name == '{0:s}':
                            matches.append(file)
            return_json(matches)
            """
        ).format(escape_single_quotes(layer_name)), timeout_in_seconds=timeout_in_seconds)

    def find_files_by_script(self, script_predicate: str, timeout_in_seconds: float = None) -> List[str]:
        """
        Find files matching certain criteria by executing a gimp script.

        If the script opens a file with **open_xcf('__file__')**, then the script is executed for each file
        and a boolean result returned by **return_bool(value)** is expected.

        If the script retrieves the whole list of files with **get_json('__files__')**, then the script is
        only executed once and passed the whole list of files as a parameter. A result returned by
        **return_json(value)** in the form of a list is expected. This solution has better performance
        but you need to make sure that memory is cleaned up between opening files, e.g. by invoking
        **gimp_image_delete(image)**.

        Example with script that is executed per file:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.util.TempFile import TempFile
        >>> from pgimp.util.string import escape_single_quotes
        >>> import numpy as np
        >>> with TempFile('_bg.xcf') as f1, TempFile('_fg.xcf') as f2:  # doctest: +ELLIPSIS
        ...     gf1 = GimpFile(f1).create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gf2 = GimpFile(f2).create('Foreground', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gfc = GimpFileCollection.create_from_gimp_files([gf1, gf2])
        ...     script = textwrap.dedent(
        ...         '''
        ...         from pgimp.gimp.file import open_xcf
        ...         from pgimp.gimp.parameter import return_bool
        ...         image = open_xcf('__file__')
        ...         for layer in image.layers:
        ...             if layer.name == '{0:s}':
        ...                 return_bool(True)
        ...         return_bool(False)
        ...         '''
        ...     ).format(escape_single_quotes('Foreground'))
        ...     gfc.find_files_by_script(script)
        ['..._fg.xcf']

        Example with script that is executed once on all files:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.util.TempFile import TempFile
        >>> from pgimp.util.string import escape_single_quotes
        >>> import numpy as np
        >>> with TempFile('_bg.xcf') as f1, TempFile('_fg.xcf') as f2:  # doctest: +ELLIPSIS
        ...     gf1 = GimpFile(f1).create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gf2 = GimpFile(f2).create('Foreground', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gfc = GimpFileCollection.create_from_gimp_files([gf1, gf2])
        ...     script = textwrap.dedent(
        ...         '''
        ...         import gimp
        ...         from pgimp.gimp.file import XcfFile
        ...         from pgimp.gimp.parameter import return_json, get_json
        ...         files = get_json('__files__')
        ...         matches = []
        ...         for file in files:
        ...             with XcfFile(file) as image:
        ...                 for layer in image.layers:
        ...                     if layer.name == '{0:s}':
        ...                         matches.append(file)
        ...         return_json(matches)
        ...         '''
        ...     ).format(escape_single_quotes('Foreground'))
        ...     gfc.find_files_by_script(script)
        ['..._fg.xcf']

        :param script_predicate: Script to be executed.
        :param timeout_in_seconds: Script execution timeout in seconds.
        :return: List of files matching the criteria.
        """
        if "open_xcf('__file__')" in script_predicate and "return_bool(" in script_predicate:
            return list(filter(lambda file: self._gsr.execute_and_parse_bool(
                script_predicate.replace('__file__', escape_single_quotes(file)),
                timeout_in_seconds=timeout_in_seconds
            ), self._files))
        if "get_json('__files__')" in script_predicate and "return_json(" in script_predicate:
            return self._gsr.execute_and_parse_json(
                script_predicate,
                parameters={'__files__': self._files},
                timeout_in_seconds=timeout_in_seconds
            )
        raise GimpMissingRequiredParameterException(
            'Either an image file must be opened with open_xcf(\'__file__\') ' +
            'and the result is returned with return_bool() ' +
            'or a list of files must be retrieved by get_json(\'__files__\') ' +
            'and the result is returned with return_json().'
        )

    def execute_script_and_return_json(
            self,
            script: str,
            parameters: dict = None,
            timeout_in_seconds: float = None
    ) -> Union[JsonType, Dict[str, JsonType]]:
        """
        Execute a gimp script on the collection.

        If the script opens a file with **open_xcf('__file__')**, then the script is executed for each file
        and a result returned by **return_json(value)** is expected. The results will be returned as a
        dictionary containing the filenames as keys and the results as values.

        If the script retrieves the whole list of files with **get_json('__files__')**, then the script is
        only executed once and passed the whole list of files as a parameter. A result returned by
        **return_json(value)** is expected. This solution has better performance
        but you need to make sure that memory is cleaned up between opening files, e.g. by invoking
        **gimp_image_delete(image)**.

        Example with script that is executed per file:

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.util.TempFile import TempFile
        >>> from pgimp.util.string import escape_single_quotes
        >>> import numpy as np
        >>> with TempFile('_bg.xcf') as f1, TempFile('_fg.xcf') as f2:  # doctest: +ELLIPSIS
        ...     gf1 = GimpFile(f1).create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gf2 = GimpFile(f2).create('Foreground', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gfc = GimpFileCollection.create_from_gimp_files([gf1, gf2])
        ...     script = textwrap.dedent(
        ...         '''
        ...         from pgimp.gimp.file import open_xcf
        ...         from pgimp.gimp.parameter import return_json
        ...         image = open_xcf('__file__')
        ...         for layer in image.layers:
        ...             if layer.name == '{0:s}':
        ...                 return_json(True)
        ...         return_json(False)
        ...         '''
        ...     ).format(escape_single_quotes('Foreground'))
        ...     gfc.execute_script_and_return_json(script)
        {'..._bg.xcf': False, '..._fg.xcf': True}

        Example with script that is executed once on all files using open_xcf():

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.util.TempFile import TempFile
        >>> from pgimp.util.string import escape_single_quotes
        >>> import numpy as np
        >>> with TempFile('_bg.xcf') as f1, TempFile('_fg.xcf') as f2:  # doctest: +ELLIPSIS
        ...     gf1 = GimpFile(f1).create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gf2 = GimpFile(f2).create('Foreground', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gfc = GimpFileCollection.create_from_gimp_files([gf1, gf2])
        ...     script = textwrap.dedent(
        ...         '''
        ...         import gimp
        ...         from pgimp.gimp.file import XcfFile
        ...         from pgimp.gimp.parameter import return_json, get_json
        ...         files = get_json('__files__')
        ...         matches = []
        ...         for file in files:
        ...             with XcfFile(file) as image:
        ...                 for layer in image.layers:
        ...                     if layer.name == '{0:s}':
        ...                         matches.append(file)
        ...         return_json(matches)
        ...         '''
        ...     ).format(escape_single_quotes('Foreground'))
        ...     gfc.execute_script_and_return_json(script)
        ['..._fg.xcf']

        Example with script that is executed once on all files using for_each_file():

        >>> from pgimp.GimpFile import GimpFile
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.util.TempFile import TempFile
        >>> from pgimp.util.string import escape_single_quotes
        >>> import numpy as np
        >>> with TempFile('_bg.xcf') as f1, TempFile('_fg.xcf') as f2:  # doctest: +ELLIPSIS
        ...     gf1 = GimpFile(f1).create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gf2 = GimpFile(f2).create('Foreground', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gfc = GimpFileCollection.create_from_gimp_files([gf1, gf2])
        ...     script = textwrap.dedent(
        ...         '''
        ...         from pgimp.gimp.file import for_each_file
        ...         from pgimp.gimp.parameter import return_json, get_json
        ...
        ...         matches = []
        ...
        ...         def layer_matches(image, file):
        ...             for layer in image.layers:
        ...                 if layer.name == '{0:s}':
        ...                     matches.append(file)
        ...
        ...         for_each_file(layer_matches)
        ...         return_json(matches)
        ...         '''
        ...     ).format(escape_single_quotes('Foreground'))
        ...     gfc.execute_script_and_return_json(script)
        ['..._fg.xcf']

        :param script: Script to be executed on the files.
        :param parameters: Parameters to pass to the script.
        :param timeout_in_seconds:  Script execution timeout in seconds.
        :return: Dictionary of filenames and results if the script reads a single file.
                 Json if the script takes the whole list of files.
        """
        parameters = parameters or {}
        if "open_xcf('__file__')" in script and "return_json(" in script:
            return {file: self._gsr.execute_and_parse_json(
                script.replace('__file__', escape_single_quotes(file)),
                parameters=parameters,
                timeout_in_seconds=timeout_in_seconds
            ) for file in self._files}
        elif ("get_json('__files__')" in script or "for_each_file(" in script) and "return_json(" in script:
            return self._gsr.execute_and_parse_json(
                script,
                parameters={**parameters, '__files__': self._files},
                timeout_in_seconds=timeout_in_seconds
            )
        else:
            raise GimpMissingRequiredParameterException(
                'Either an image file must be opened with open_xcf(\'__file__\') ' +
                'and the result is returned with return_json() ' +
                'or a list of files must be retrieved by get_json(\'__files__\') or for_each_file() ' +
                'and the result is returned with return_json().'
            )

    def copy_layer_from(
            self,
            other_collection: 'GimpFileCollection',
            layer_name: str,
            layer_position: int = 0,
            other_can_be_smaller: bool = False,
            timeout_in_seconds: float = None
    ) -> 'GimpFileCollection':
        """
        Copies a layer from another collection into this collection.

        Example:

        >>> from tempfile import TemporaryDirectory
        >>> import numpy as np
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.GimpFile import GimpFile
        >>> with TemporaryDirectory('_src') as srcdir, TemporaryDirectory('_dst') as dstdir:  # doctest: +ELLIPSIS
        ...     src_1 = GimpFile(os.path.join(srcdir, 'file1.xcf')) \\
        ...         .create('Background', np.zeros(shape=(1, 1), dtype=np.uint8)) \\
        ...         .add_layer_from_numpy('White', np.ones(shape=(1, 1), dtype=np.uint8)*255)
        ...     src_2 = GimpFile(os.path.join(srcdir, 'file2.xcf')) \\
        ...         .create('Background', np.zeros(shape=(1, 1), dtype=np.uint8)) \\
        ...         .add_layer_from_numpy('White', np.ones(shape=(1, 1), dtype=np.uint8)*255)
        ...
        ...     dst_1 = GimpFile(os.path.join(dstdir, 'file1.xcf')) \\
        ...         .create('Background', np.zeros(shape=(1, 1), dtype=np.uint8)) \\
        ...         .add_layer_from_numpy('White', np.zeros(shape=(1, 1), dtype=np.uint8)*255)
        ...     dst_2 = GimpFile(os.path.join(dstdir, 'file2.xcf')) \\
        ...         .create('Background', np.zeros(shape=(1, 1), dtype=np.uint8))
        ...
        ...     src_collection = GimpFileCollection([src_1.get_file(), src_2.get_file()])
        ...     dst_collection = GimpFileCollection([dst_1.get_file(), dst_2.get_file()])
        ...
        ...     dst_collection.copy_layer_from(src_collection, 'White', layer_position=1, timeout_in_seconds=10)
        ...
        ...     np.all(dst_1.layer_to_numpy('White') == 255) \\
        ...         and ['Background', 'White'] == dst_1.layer_names() \\
        ...         and 'White' in dst_2.layer_names() \\
        ...         and np.all(dst_2.layer_to_numpy('White') == 255) \\
        ...         and ['Background', 'White'] == dst_2.layer_names()
        <...>
        True

        :param other_collection: The collection from which to take the layer.
        :param layer_name: Name of the layer to copy.
        :param layer_position: Layer position in the destination image.
        :param other_can_be_smaller: Whether the other collection must at least contain all the
               elements of the current collection or not.
        :param timeout_in_seconds: Script execution timeout in seconds.
        :return: :py:class:`~pgimp.GimpFileCollection.GimpFileCollection`
        """
        prefix_in_other_collection = other_collection.get_prefix()
        files_in_other_collection = map(lambda file: file[len(prefix_in_other_collection):], other_collection._files)
        prefix_in_this_collection = self.get_prefix()
        files_in_this_collection = map(lambda file: file[len(prefix_in_this_collection):], other_collection._files)
        missing = set()
        if not other_can_be_smaller:
            missing = set(files_in_this_collection) - set(files_in_other_collection)
        if missing:
            raise MissingFilesException(
                'The other collection is smaller than this collection by the following entries: ' +
                ', '.join(missing)
            )

        script = textwrap.dedent(
            """
            import os
            from pgimp.gimp.parameter import get_json, get_string, get_int, get_bool, return_json
            from pgimp.gimp.layer import copy_or_merge_layer
            from pgimp.gimp.file import XcfFile

            prefix_in_other_collection = get_string('prefix_in_other_collection')
            prefix_in_this_collection = get_string('prefix_in_this_collection')
            layer_name = get_string('layer_name')
            layer_position = get_int('layer_position')
            other_can_be_smaller = get_bool('other_can_be_smaller')
            files = get_json('__files__')

            for file in files:
                file = file[len(prefix_in_this_collection):]
                file_src = os.path.join(prefix_in_other_collection, file)
                file_dst = os.path.join(prefix_in_this_collection, file)
                if other_can_be_smaller and not os.path.exists(file_src):
                    continue

                with XcfFile(file_src) as image_src, XcfFile(file_dst, save=True) as image_dst:
                    copy_or_merge_layer(image_src, layer_name, image_dst, layer_name, layer_position)

            return_json(None)
            """
        )

        self.execute_script_and_return_json(
            script,
            parameters={
                'prefix_in_other_collection': prefix_in_other_collection,
                'prefix_in_this_collection': prefix_in_this_collection,
                'layer_name': layer_name,
                'layer_position': layer_position,
                'other_can_be_smaller': other_can_be_smaller,
            },
            timeout_in_seconds=timeout_in_seconds
        )
        return self

    def merge_mask_layer_from(
            self, other_collection: 'GimpFileCollection',
            layer_name: str,
            mask_foreground_color: MaskForegroundColor = MaskForegroundColor.WHITE,
            layer_position: int = 0,
            timeout_in_seconds: float = None
    ):
        """
        Merge masks together. Masks should contain grayscale values or have an rgb gray value (r, g, b)
        with r == g == b. In case of rgb, the componentwise minimum or maximum will be taken depending
        on the foreground color. When the mask foreground color is white, then the maximum of values is
        taken when merging. Otherwise the minimum is taken.

        Example:

        >>> from tempfile import TemporaryDirectory
        >>> import numpy as np
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.GimpFile import GimpFile
        >>> with TemporaryDirectory('_src') as srcdir, TemporaryDirectory('_dst') as dstdir:  # doctest: +ELLIPSIS
        ...     src_1 = GimpFile(os.path.join(srcdir, 'file1.xcf')) \\
        ...         .create('Mask', np.array([[255, 0]], dtype=np.uint8))
        ...
        ...     dst_1 = GimpFile(os.path.join(dstdir, 'file1.xcf')) \\
        ...         .create('Mask', np.array([[0, 255]], dtype=np.uint8))
        ...     dst_2 = GimpFile(os.path.join(dstdir, 'file2.xcf')) \\
        ...         .create('Mask', np.array([[0, 255]], dtype=np.uint8))
        ...
        ...     src_collection = GimpFileCollection([src_1.get_file()])
        ...     dst_collection = GimpFileCollection([dst_1.get_file(), dst_2.get_file()])
        ...
        ...     dst_collection.merge_mask_layer_from(
        ...         src_collection, 'Mask', MaskForegroundColor.WHITE, timeout_in_seconds=10)
        ...
        ...     np.all(dst_1.layer_to_numpy('Mask') == [[255], [255]]) \\
        ...         and ['Mask'] == dst_1.layer_names() \\
        ...         and 'Mask' in dst_2.layer_names() \\
        ...         and np.all(dst_2.layer_to_numpy('Mask') == [[0], [255]]) \\
        ...         and ['Mask'] == dst_2.layer_names()
        <...>
        True

        :param other_collection: The collection from which to merge the mask.
        :param layer_name: Name of the layer to copy.
        :param mask_foreground_color: when white, the maximum is taken, when black, the minimum values are taken
               when merging
        :param layer_position: Layer position in the destination image.
        :param timeout_in_seconds: Script execution timeout in seconds.
        :return: :py:class:`~pgimp.GimpFileCollection.GimpFileCollection`
        """
        prefix_in_other_collection = other_collection.get_prefix()
        prefix_in_this_collection = self.get_prefix()

        script = textwrap.dedent(
            """
            import os
            from pgimp.gimp.file import XcfFile
            from pgimp.gimp.parameter import get_json, get_string, get_int, return_json
            from pgimp.gimp.layer import merge_mask_layer

            prefix_in_other_collection = get_string('prefix_in_other_collection')
            prefix_in_this_collection = get_string('prefix_in_this_collection')
            layer_name = get_string('layer_name')
            layer_position = get_int('layer_position')
            mask_foreground_color = get_int('mask_foreground_color')
            files = get_json('__files__')

            for file in files:
                file = file[len(prefix_in_this_collection):]
                file_src = os.path.join(prefix_in_other_collection, file)
                file_dst = os.path.join(prefix_in_this_collection, file)
                if not os.path.exists(file_src):
                    continue
                with XcfFile(file_src) as image_src, XcfFile(file_dst, save=True) as image_dst:
                    merge_mask_layer(
                        image_src,
                        layer_name,
                        image_dst,
                        layer_name,
                        mask_foreground_color,
                        layer_position
                    )

            return_json(None)
            """
        )

        self.execute_script_and_return_json(
            script,
            parameters={
                'prefix_in_other_collection': prefix_in_other_collection,
                'prefix_in_this_collection': prefix_in_this_collection,
                'layer_name': layer_name,
                'layer_position': layer_position,
                'mask_foreground_color': mask_foreground_color.value,
            },
            timeout_in_seconds=timeout_in_seconds
        )
        return self

    def clear_selection(
        self,
        timeout_in_seconds: float = None
    ):
        """
        Clears active selections.

        :param timeout_in_seconds: Script execution timeout in seconds.
        """
        script = textwrap.dedent(
            """
            import gimp
            from pgimp.gimp.parameter import get_json, return_json
            from pgimp.gimp.file import XcfFile
            
            files = get_json('__files__')
            for file in files:
                with XcfFile(file, save=True) as image:
                    gimp.pdb.gimp_selection_none(image)
            
            return_json(None)
            """
        )
        self.execute_script_and_return_json(
            script,
            timeout_in_seconds=timeout_in_seconds
        )

    def remove_layers_by_name(
        self,
        layer_names: List[str],
        timeout_in_seconds: float = None
    ):
        """
        Removes layers by name.

        Example:

        >>> from tempfile import TemporaryDirectory
        >>> import numpy as np
        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from pgimp.GimpFile import GimpFile
        >>> data = np.array([[0, 255]], dtype=np.uint8)
        >>> with TemporaryDirectory('_files') as dir:
        ...    file1 = GimpFile(os.path.join(dir, 'file1.xcf')) \\
        ...        .create('Background', data) \\
        ...        .add_layer_from_numpy('Layer 1', data) \\
        ...        .add_layer_from_numpy('Layer 2', data) \\
        ...        .add_layer_from_numpy('Layer 3', data)
        ...    file2 = GimpFile(os.path.join(dir, 'file2.xcf')) \\
        ...        .create('Background', data) \\
        ...        .add_layer_from_numpy('Layer 1', data) \\
        ...        .add_layer_from_numpy('Layer 2', data)
        ...
        ...    collection = GimpFileCollection([file1.get_file(), file2.get_file()])
        ...    collection.remove_layers_by_name(['Layer 1', 'Layer 3'], timeout_in_seconds=10)
        ...
        ...    [file1.layer_names(), file2.layer_names()]
        [['Layer 2', 'Background'], ['Layer 2', 'Background']]

        :param layer_names: List of layer names.
        :param timeout_in_seconds: Script execution timeout in seconds.
        """
        script = textwrap.dedent(
            """
            import gimp
            from pgimp.gimp.parameter import get_json, return_json
            from pgimp.gimp.file import XcfFile
            
            files = get_json('__files__')
            layer_names = get_json('layer_names')
            for file in files:
                with XcfFile(file, save=True) as image:
                    for layer_name in layer_names:
                        layer = gimp.pdb.gimp_image_get_layer_by_name(image, layer_name)
                        if layer is not None:
                            gimp.pdb.gimp_image_remove_layer(image, layer)
            
            return_json(None)
            """
        )
        self.execute_script_and_return_json(
            script,
            parameters={'layer_names': layer_names},
            timeout_in_seconds=timeout_in_seconds
        )

    @classmethod
    def create_from_pathname(cls, pathname: str):
        """
        Create a collection of gimp files by pathname.

        Example:

        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from tempfile import TemporaryDirectory
        >>> import numpy as np
        >>> with TemporaryDirectory('gfc') as tmpdir:
        ...     gf1 = GimpFile(os.path.join(tmpdir, 'f1.xcf')) \\
        ...         .create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gf2 = GimpFile(os.path.join(tmpdir, 'f2.xcf')) \\
        ...         .create('Foreground', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gfc = GimpFileCollection.create_from_pathname(tmpdir)
        ...     gfc.replace_prefix(gfc.get_prefix()).get_files()
        ['f1.xcf', 'f2.xcf']

        :param pathname: Can be a file with or without .xcf suffix, directory or recursive directory search.
                         Allowed wildcards include '*' for matching zero or more characters
                         and '**' for recursive search.
        :return: A :py:class:`~pgimp.GimpFileCollection.GimpFileCollection` that contains an ordered list of filenames.
        """
        if pathname.endswith('**') or pathname.endswith('**/'):
            pathname = os.path.join(pathname, '*' + EXTENSION)
        elif os.path.isdir(pathname):  # only search for gimp files in dir
            pathname = os.path.join(pathname, '*' + EXTENSION)
        else:  # only search for gimp files and add extension if necessary
            base, extension = os.path.splitext(pathname)
            if extension != EXTENSION and extension != '':
                return cls([])
            pathname = base + EXTENSION

        files = glob(pathname, recursive=True)
        files = sorted(files, key=lambda file: (file.count('/'), file))
        return cls(list(files))

    @classmethod
    def create_from_gimp_files(cls, gimp_files: List[GimpFile]):
        """
        Create a collection of gimp files by a list of :py:class:`~pgimp.GimpFile.GimpFile`.

        Example:

        >>> from pgimp.GimpFileCollection import GimpFileCollection
        >>> from tempfile import TemporaryDirectory
        >>> import numpy as np
        >>> with TemporaryDirectory('gfc') as tmpdir:
        ...     gf1 = GimpFile(os.path.join(tmpdir, 'f1.xcf')) \\
        ...         .create('Background', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gf2 = GimpFile(os.path.join(tmpdir, 'f2.xcf')) \\
        ...         .create('Foreground', np.zeros(shape=(2, 2), dtype=np.uint8))
        ...     gfc = GimpFileCollection.create_from_gimp_files([gf1, gf2])
        ...     gfc.replace_prefix(gfc.get_prefix()).get_files()
        ['f1.xcf', 'f2.xcf']

        :param gimp_files: The list of gimp files to be contained in the collection.
        :return: A :py:class:`~pgimp.GimpFileCollection.GimpFileCollection` that contains an ordered list of filenames.
        """

        return cls(list(map(lambda f: f.get_file(), gimp_files)))