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
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
def __init__(self, output: Output) -> None: super().__init__() self._output = output self._gsr = GimpScriptRunner() self._ordered_gimp_classes = []
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)
# Copyright 2018 Mathias Burger <*****@*****.**> # # SPDX-License-Identifier: MIT import numpy as np import pytest from pgimp.GimpScriptRunner import GimpScriptRunner, GimpScriptException gsr = GimpScriptRunner() @pytest.mark.parametrize("test_input,expected", [ (True, "True"), (False, "False"), ]) def test_get_bool(test_input, expected): out = gsr.execute( "from pgimp.gimp.parameter import *; import sys; print(get_bool('param'))", parameters={'param': test_input}, timeout_in_seconds=1) assert "{:s}\n".format(expected) == out def test_get_int(): out = gsr.execute( "from pgimp.gimp.parameter import *; import sys; print(get_int('param'))", parameters={'param': 21}, timeout_in_seconds=1)
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()
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)))