#!/usr/bin/env python # This plugin was created by the GIMP plugin "GimpScripter..." i.e. plugin-gimpscripter.py # This *wrapper* plugin calls one or more *wrapped* or *target* plugins or PDB procedures. # Below, "# <=" indicates lines that had substitutions by GimpScripter ''' Runtime "library" for a Gimp wrapper plugin. Runtime means can be called by the wrapper plugin when it runs. Not needed or present in all wrapper plugins. You are probably reading this in a wrapper plugin generated by GimpScripter. The source for this file is gimpscripter/runtime.py ''' from gimpfu import * ''' Classes for shadowing Gimp objects so that we can infer their creation and deletion by commands in the wrapper plugin and maintain a stack whose top can be referred to by commands. ''' class GimpStack(list): ''' GimpScripter stack of active Gimp objects.''' def __init__(self, type_on_stack, an_object=None): self.type_on_stack = type_on_stack # Readable type, not the encoding PF_foo # For init, an_object MUST be a formal parameter of wrapper plugin. # Otherwise, what would an_object refer to? if an_object: self.append(an_object) self.stack_top = 0 else: # At initialization time, there is no object of this type known to be active. self.stack_top = -1 def __str__(self): return "GimpStack type " + self.type_on_stack def push(self, an_object): self.stack_top += 1 # preincrement self.append(an_object) print "Pushed ", an_object.name, " to stack type", self.type_on_stack, " position ", self.stack_top def pop(self): ''' assert the top object has been deleted from Gimp Ours may be the last reference to it. Can we still get its name? If so, for robustness we should check we are deleting the name we inferred is gone. ''' del(self[self.stack_top]) # NOT remove, we don't need the object self.stack_top -= 1 print "Popped. New top is ", self[self.stack_top].name, " at position ", self.stack_top def top(self): try: return self[self.stack_top] except IndexError: # Probably the stack is empty. Rather than return None, abort. pdb.gimp_message("This wrapper plugin can't find active object of type: %s." % self.type_on_stack) raise RuntimeError("Plugin failed to find an active image, drawable, layer, or channel.") class GimpEphemera(object): ''' GimpScripter's shadow of Gimp objects ''' def __init__(self, a_image, a_drawable): ''' Must be initialized to the ephemera existing when Ephemera instance created which is in the first line of plugin_main. Ephemera existing then are captured in ephemera but not in stacks. Only the image and drawable passed to a wrapper are then stacked. Only objects created by a wrapper are subsequently stacked. Thus a wrapper can not remove, but reference to stack top, any object the wrapper did not create, except for the passed image and drawable. FUTURE: revisit this, possibly call gimp_image_get_foo to initialize stacks, if Gimp also is reliably using a stack model, with an active instance for each type. ''' self.ephemera = {} self._update_ephemera() self.prior_ephemera = self._copy_ephemera_keys() # prior_ephemera is keys only, the values are not ephemeral Gimp objects # since those may be deleted by commands in wrapper plugin # Dictionary of stacks, one for each type of ephemeral self.stacks = {} self.stacks[PF_VECTORS] = GimpStack("Path") # Path stack always empty # The other stacks are initialized by passed image and drawable, or empty. self.stacks[PF_IMAGE] = GimpStack("Image", a_image) self.stacks[PF_DRAWABLE] = GimpStack("Drawable", a_drawable) # We put a drawable on two stacks if pdb.gimp_drawable_is_layer(a_drawable): # a_drawable.type == PDB_LAYER: # Doesn't work?? self.stacks[PF_LAYER] = GimpStack("Layer", a_drawable) # If a layer was passed in, channel stack is empty self.stacks[PF_CHANNEL] = GimpStack("Channel") elif pdb.gimp_drawable_is_channel(a_drawable): # a_drawable.type == PF_CHANNEL: self.stacks[PF_CHANNEL] = GimpStack("Channel", a_drawable) self.stacks[PF_LAYER] = GimpStack("Layer") else: print "Unknown drawable type", a_drawable.type raise RuntimeError("Unknown drawable type") # TODO other ephemerals??, etc. def _update_ephemera(self): ''' Update our dictionary of ephemera. ''' self.ephemera = {} # clear, start anew images = gimp.image_list() # what exists now for image in images: # !!! Note layers, channels, vectors belong to images # but we chunk them all into ephemera together, # not distinguishing two names of the same type on different images. # TODO could be a problem for some wrapper use cases print image.name # !!! Note name is often "Untitled" and image.filename is None # At one time, I used filename but why?? self.ephemera[(image.name, PF_IMAGE)] = image for layer in image.layers: print "Layer ", layer.name # !!!! Two entries. Each will go on their own stack. self.ephemera[(layer.name, PF_LAYER)] = layer self.ephemera[(layer.name, PF_DRAWABLE)] = layer for channel in image.channels: print "Channel ", channel.name self.ephemera[(channel.name, PF_CHANNEL)] = channel self.ephemera[(channel.name, PF_DRAWABLE)] = channel # !!!! Two entries # vector aka path for vector in image.vectors: print "Path ", vector.name self.ephemera[(vector.name, PF_VECTORS)] = vector # !!! VECTORS with an S def _update_stack(self, a_type): ''' Update our stack of the given type''' # make dictionaries by name for objects of type, from ephemera now and then now = self._dict_by_name(self.ephemera, a_type) then = self._dict_by_name(self.prior_ephemera, a_type) # Diff to find a recently created object diff_name = set(now)-set(then) # difference dictionary keys using sets if len(diff_name) == 1: # TODO filename for images??? for name in diff_name: # Only one, but can't index sets self.stacks[a_type].push(now[name]) # push recently created object elif len(diff_name) > 1: print diff_name raise RuntimeError("Many ephemera of same type created by one command") # else zero, pass # Diff to find recently deleted object diff_name = set(then)-set(now) # difference dictionary keys using sets if len(diff_name) == 1: for name in diff_name: # Only one, but can't index sets self.stacks[a_type].pop() # pop recently deleted object then[name] elif len(diff_name) > 1: print diff_name raise RuntimeError("Many ephemera deleted by one command") def _update_stacks(self): ''' Update our stacks of ephemera. ''' # !!! this list must match the one in parameters.py for a_type in (PF_IMAGE, PF_DRAWABLE, PF_LAYER, PF_CHANNEL, PF_VECTORS): self._update_stack(a_type) def _copy_ephemera_keys(self): ''' Return a dictionary whose keys are copy of ephemera. Values are not needed. ''' result = {} for (keyname, keytype) in self.ephemera.iterkeys(): result[(keyname, keytype)] = True return result def update(self): ''' Refresh ephemera by querying Gimp ''' self._update_ephemera() self._update_stacks() # diff ephemera and prior_ephemera # !!! Remember keys of prior_ephemera for the next update self.prior_ephemera = self._copy_ephemera_keys() def _dict_by_name(self, a_ephemera, a_type): ''' Return dict keyed by name of ephemera of type ''' result = {} for (keyname, keytype) in a_ephemera.iterkeys(): if keytype == a_type: result[keyname] = a_ephemera[(keyname, keytype)] return result def top(self, a_type): ''' Return ephemeral object for stack of active object of given type. ''' result = self.stacks[a_type].top() print "Top ", str(self.stacks[a_type]), result.name return result def lookup(self, a_type, a_name): ''' Return ephemeral object having given name and type. Drawable is the super class for layers and channels If looking up a drawable, return any layer or a channel or drawable having given name. If looking up more specifically a layer (or channel) return only a layer or channel of given name ''' for (keyname, keytype) in self.ephemera.iterkeys(): if a_name == keyname: if a_type == keytype or ( a_type == PF_DRAWABLE and ( keytype == PF_LAYER or keytype == PF_CHANNEL )): return self.ephemera[(keyname, keytype)] ''' !!! Note we omit REGION and DISPLAY, no need for them in wrapper plugins? !!! Note that PF_REGION is deprecated since gimp-2.7 ?? ''' if a_type in (PF_DISPLAY, ): # !!! Note the comma to make this a tuple raise RuntimeError("Wrapper plugins do not support lookup for this type.") ''' If we get here, failed to lookup ephemeral instance by name. Display error in status line. Don't raise exception that will imply the plugin crashed. This is USUALLY wrapping-user error, not establishing ephemera that are preconditions to wrapper plugin. However, it could that author-user constructed a bad wrapper plugin, using wrong names for ephemera. Lastly, it could be a bug in GimpScripter. ''' print "Lookup failed on %s" % a_name pdb.gimp_message("This wrapper plugin can't run without object named: %s." % a_name) # TODO the user can miss this. Make it a dialog, but how? raise RuntimeError also is silent def plugin_main(image, drawable, ): # <= formal parameters # Call the wrapped procedures. # If the wrapped procedure requires (image, drawable), they are passed through. # Any other non-constant arguments have names which match formal parameters to plugin_main above # and paramdefs in register() below: they are deferred and a Gimp dialog will ask user for values. ephemera = GimpEphemera(image, drawable) # <= prelude # ephemera.update() # Edit/Copy pdb.gimp_edit_copy( ephemera.top(PF_DRAWABLE), ) ephemera.update() # Image/Mode/Indexed pdb.gimp_image_convert_indexed( ephemera.top(PF_IMAGE), 0, 0, 75, 0, 0, 'Spot Color Palette', ) ephemera.update() # Image/Mode/RGB pdb.gimp_image_convert_rgb( ephemera.top(PF_IMAGE), ) ephemera.update() # Edit/Paste pdb.gimp_edit_paste( ephemera.top(PF_DRAWABLE), 1, ) ephemera.update() # Layer/Set Mode pdb.gimp_layer_set_mode( ephemera.top(PF_LAYER), 19, ) ephemera.update() # Layer/Anchor pdb.gimp_floating_sel_anchor( ephemera.top(PF_LAYER), ) # <= body # # <= postlude if __name__ == "__main__": # invoked at top level, from GIMP from gimpfu import * gettext.install("gimp20-python", gimp.locale_directory, unicode=True) register( "python-fu-Dramatic_VectorStyle", # <= procedure name "blurb", # <= blurb "This plugin was created using 'GimpScripter...'", "HaryArt", "GimpModify", "3.02.2013", "Dramatic_VectorStyle", # <= menu item "", # <= image type [(PF_IMAGE, "image", "Input image", None), (PF_DRAWABLE, "drawable", "Input drawable", None)], # <= hidden and deferred parameters [], plugin_main, menu="/Filters/HaryArt", # <= menu path domain=("gimp20-python", gimp.locale_directory)) main()