#!/usr/bin/env python # -*- coding: utf-8 -*- # Screenshot Tearoff Effect # Copyright (c) 2008-2010 Edgar D'Souza # Contact: edgar.b.dsouza@gmail.com # --------------------------------------------------------------------- # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . #====================================================================== #Version: 0.2 2010-07-05 -- Tracy S. Fitch (thespian at bigfoot.com) -- Windows Compatible # Known Issues: # - still no real testing -- just one more platform: # - Windows 7 (Version 6.1.7600) # - GIMP 2.6.9 # - Python 2.6.4 # - pygtk-2.16 # - pycairo-1.8.6 # - pygobject-2.20 # # Changes: # - switched to current directory log file (should be Linux/Win safe) # - divided logging into progress, debug, error, & none # - added progress bar updates & pulses # - moved menu path to a separate value in register statement # - moved to filters->distorts since it seems similar to page curl # - added optional border function (default on) # - modified resizing after drop shadow to only add in the shadow direction # - rearranged layers to put shadow on the bottom # - linked shadow and border layers to the original image # - optional merging of border and shadow layers (default on) # - formatted dialog (to the extent I can figure out how to format gimpfu's dialog) # # Thoughts for the future: # - probably time to move past gimpfu and create a gimp dialog directly # - look at storing settings past gimp restart (but add a "reset to defaults" button # - move tearaway into a separate image and allow acceptance/rejection for a redo # - either make add_alpha mandatory or: # - find a way to fix drop shadow behaviour when resizing larger than base image # - fix feathered edge borders # - investigate ways to diminish rounding off corners on adjacent "torn" edges #---------------------------------------------------------------------- #Version: 0.1 -- Edgar D'Souza -- initial release. # Known issues: # - hasn't been tested on anything except the development platform - my laptop :) # # GIMP Python info taken from: http://www.gimp.org/docs/python/index.html # # This script was developed and tested under the following environment: # - Ubuntu Linux 7.10 # - Python 2.5.1 # - GIMP 2.4.2 # - gimp-python 2.4.2-0ubuntu0.7.10.1 # - libgimp2.0 2.4.2-0ubuntu0.7.10.1 # # Before invoking the plugin: # 1. Capture a new screenshot, or open an existing image file. # 2. Select the part to keep, making the selection a little larger than what # you want to keep. GIMP does appear to distress the selection "outward", # but verify that you're not losing image data that you want to keep. Since # a few more pixels than needed are probably not going to be fatal :) I'm # suggesting selecting a little more than you need to keep. from gimpfu import * gettext.install("gimp20-python", gimp.locale_directory, unicode=True) global tearoff_loglevel #Uncomment one of the following loglevels #tearoff_loglevel = 'Debug' # Error and Debug messages logged; Logfile wiped/created on plugin start tearoff_loglevel = 'Error' # Error messages logged; Debug messages suppressed; Logfile wiped/created on first error #tearoff_loglevel = 'None' # All messages suppressed; Logfile unchanged #Helper function - write messages to a log file. def tear_log(msg, init_file=False): """Opens a log file, writes the string in the msg argument, closes the file. The optional init_file argument, if True, (re)initializes the file. """ global log_opened global tearoff_loglevel if not tearoff_loglevel == 'None': filename = ('gimp_plugin_tearoff.log') dumpstr = "tearoff: " + msg + '\n' if log_opened: tmpfile = open(filename, 'a') else: tmpfile = open(filename, 'w') if tearoff_loglevel == 'Debug': tmpfile.write('tearoff: Logging initiated: Errors & Debug included\n') elif tearoff_loglevel == 'Error': tmpfile.write('tearoff: Logging initiated: Errors included; Debug suppressed\n') log_opened = True tmpfile.write(dumpstr) tmpfile.close() #Observation: the log file contains only one startup and invocation block, no matter #how many times I invoke the plugin on a single image window. This implies that it's #being torn down and re-initialized after every invocation. A bit expensive, perhaps, #but I suppose it enables better garbage collection...just a guess. #Helper function - writes debug messages to a log file. def debug_log(msg): global tearoff_loglevel #filter out debug messages if we're in 'Error' or 'None' log mode if tearoff_loglevel == 'Debug': tear_log('debug: ' + msg) else: return #Helper function - writes error messages to a log file. def error_log(msg): #Fires the real log function for error messages tear_log('error: ' + msg) #Helper function - used for debug messages that should also update progress. def progress_log(msg): debug_log(msg) pdb.gimp_progress_set_text('Tearoff: ' + msg) pdb.gimp_progress_pulse() #The main plugin function that manipulates the image. def tearoff(the_img, the_drawable, distort_threshold, distort_spread, distort_granularity, distort_smooth_level, add_alpha, allow_resize, add_dropshadow, ds_offset_x, ds_offset_y, ds_blur_radius, ds_color, ds_opacity, add_border, bdr_offset, bdr_color, bdr_edge, crop, crp_tightness, merge_layers ): #Applies the tearoff effect to the image. #Parameters expected are those defined in the plugin_params_list below: gimp.progress_init("Creating tearoff") debug_log('Function main() started...') # 1a. Check that there is a valid selection. try: sel_empty = pdb.gimp_selection_is_empty(the_img) #See "name weirdness" note below. progress_log('Confirmed selection is non-empty') except BaseException, error: error_log('Calling pdb.gimp_selection_is_empty: error: ' + str(error)) if sel_empty: error_log("Error: called without a selection for the image!") gimp.message('This plugin needs a selection in the image; please select an area of the image and re-run the plugin.\n\nPlugin will now exit.') return False # 1b. Start an undo group on the image, so that all steps done in the plugin can be undone in # one user undo step (Ctrl-Z) progress_log('Opening undo group...') try: pdb.gimp_image_undo_group_start(the_img) except BaseException, error: error_log('Calling pdb.gimp_image_undo_group_start: error: ' + str(error)) # 1c. Save active layer for later progress_log('Tagging active layer...') try: user_layer = the_img.active_layer except BaseException, error: error_log('Storing user_layer from img.active_layer: error: ' + str(error)) # 2. Distress the selection, passing parameters obtained from UI. GIMP is smart # - it only distresses the edge of the selection that has image area beyond it # (i.e. the selection edge is NOT up against the edge of the image). progress_log('Distressing selection...') try: pdb.script_fu_distress_selection(the_img, the_drawable,distort_threshold, distort_spread, distort_granularity, distort_smooth_level, True, True) # The True values are to force smoothing horiz and vert; looks awful otherwise. except BaseException, error: error_log('Calling script_fu_distress_selection: error: ' + str(error)) # 3a. Found that re-inverting the selection in code after cutting out # image content doesn't work (whole layer is selected) so am saving # the active selection here and will load it later. progress_log('Saving selection...') try: saved_selection_channel = pdb.gimp_selection_save(the_img) except BaseException, error: error_log('Calling gimp_selection_save for original image: error: ' + str(error)) # 3b. Invert the selection, so what is selected can be deleted. progress_log('Inverting selection...') try: pdb.gimp_selection_invert(the_img) except BaseException, error: error_log('Calling gimp_selection_invert: error: ' + str(error)) # 4. Check if image has alpha channel (transparency) or add it (GUI: Layer > # Transparency > Add Alpha Channel) progress_log('Checking/adding alpha...') if not add_alpha: debug_log('Parameter passed from UI said not to add alpha channel') else: try: if the_drawable.has_alpha > 0: pass else: pdb.gimp_layer_add_alpha(the_drawable) except BaseException, error: error_log('Checking/adding alpha channel: error: ' + str(error)) # 5. Delete selection (leaving behind transparent/background-filled area). progress_log('Deleting unselected...') try: del_buf_name = pdb.gimp_edit_named_cut(the_drawable, "del_buf") pdb.gimp_buffer_delete(del_buf_name) #Couldn't find a 'delete selection' method... but this works? #Cutting to a named buffer, and then deleting it, avoids leaving the cut #image data on the clipboard, which ugly outcome you face when using just #plain gimp-edit-cut(). except BaseException, error: error_log('Trying to delete (inverted) selected area: error: ' + str(error)) # 6. Invert the selection again, selecting the part we want to keep. # Found that re-inverting the selection in code after cutting out # image content doesn't work (whole layer is selected) so I load # a selection that I saved earlier in step 3a. try: pdb.gimp_selection_none(the_img) pdb.gimp_selection_load(saved_selection_channel) except BaseException, error: error_log('Loading saved selection after deleting unselected: error: ' + str(error)) # 7. Add Border if not add_border: debug_log('Parameter passed from UI said not to add border') else: progress_log('Adding border...') # check whether to resize image to allow for borders if not allow_resize: debug_log('Parameter passed from UI said not to resize for border') else: # resize image to allow for borders new_width = int(the_img.width + 2 * bdr_offset) new_height = int(the_img.height + 2 * bdr_offset) debug_log( 'Resizing for border: (' + str(the_img.width) + ', ' + str(the_img.height) + ')==>(' + str(new_width) + ', ' + str(new_height) + ')...' ) try: pdb.gimp_image_resize( the_img, new_width, new_height, bdr_offset, bdr_offset) except BaseException, error: error_log('Resizing image for border size: error: ' + str(error)) if bdr_edge == 1: # increase selection size by border offset progress_log('Growing selection for hard bordered size...') try: pdb.gimp_selection_grow(the_img, bdr_offset) except BaseException, error: error_log('Calling gimp_selection_grow for hard bordered size: error: ' + str(error)) elif bdr_edge == 0: # make feathered border (internal overlap doesn't matter) progress_log('Bordering selection for feathered bordered size...') try: pdb.gimp_selection_border(the_img, bdr_offset * 2) except BaseException, error: error_log('Calling gimp_selection_border for feathered bordered size: error: ' + str(error)) else: error_log('Unsupported bdr_edge value: error: ' + str(bdr_edge)) # save new selection to a channel to manipulate for border progress_log('Saving bordered selection...') try: border_selection_channel = pdb.gimp_selection_save(the_img) except BaseException, error: error_log('Calling gimp_selection_save for border: error: ' + str(error)) # also save selection to a channel to keep for border try: saved_border_selection_channel = pdb.gimp_selection_save(the_img) except BaseException, error: error_log('Calling gimp_selection_save for saved border: error: ' + str(error)) # subtract smaller original selection from new larger selection to make a border sized channel progress_log('Defining border area...') try: pdb.gimp_channel_combine_masks(border_selection_channel, saved_selection_channel, 1, 0, 0) except BaseException, error: error_log('Calling gimp_channel_combine_masks for border area : error: ' + str(error)) # set selection to the border channel try: pdb.gimp_selection_none(the_img) pdb.gimp_selection_load(border_selection_channel) except BaseException, error: error_log('Loading border area selection: error: ' + str(error)) # create a layer for the border progress_log('Creating border layer...') try: bdr_layer = pdb.gimp_layer_new( the_img, new_width, new_height, 1, "Border", 100, 0) except BaseException, error: error_log('Creating border layer: error: ' + str(error)) # set bdr_layer and user_layer linked try: pdb.gimp_drawable_set_linked(bdr_layer, True) except BaseException, error: error_log('Linking border layer: error: ' + str(error)) try: pdb.gimp_drawable_set_linked(user_layer, True) except BaseException, error: error_log('Linking user layer: error: ' + str(error)) # add the border layer to the image try: pdb.gimp_image_add_layer(the_img, bdr_layer, -1) except BaseException, error: error_log('Adding border layer to image: error: ' + str(error)) # make entire border layer's fill transparent progress_log('Filling border area...') try: pdb.gimp_drawable_fill(bdr_layer,3) except BaseException, error: error_log('Setting border layer to transparent fill: error: ' + str(error)) # save existing foreground color try: user_color = pdb.gimp_context_get_foreground() except BaseException, error: error_log('Retrieving/saving existing foreground color: error: ' + str(error)) # set foreground color to border color try: pdb.gimp_context_set_foreground(bdr_color) except BaseException, error: error_log('Applying border color as foreground color: error: ' + str(error)) # fill border selection with foreground color try: pdb.gimp_edit_fill(bdr_layer,0) except BaseException, error: error_log('Setting border selection to foreground fill: error: ' + str(error)) # restore original foreground color try: pdb.gimp_context_set_foreground(user_color) except BaseException, error: error_log('Restoring existing foreground color: error: ' + str(error)) # add smaller original selection to new larger selection for bordered sized channel progress_log('Selecting bordered area...') try: pdb.gimp_channel_combine_masks(saved_selection_channel, saved_border_selection_channel, 0, 0, 0) except BaseException, error: error_log('Calling gimp_channel_combine_masks for bordered area : error: ' + str(error)) # set selection to the border channel try: pdb.gimp_selection_none(the_img) pdb.gimp_selection_load(saved_selection_channel) except BaseException, error: error_log('Loading bordered area selection: error: ' + str(error)) # resize user_layer to match possibly modified image size progress_log('Resizing user layer to match bordered image...') try: pdb.gimp_layer_resize_to_image_size(user_layer) except BaseException, error: error_log('Calling gimp_layer_resize_to_image_size on stored user layer: error: ' + str(error)) # reselecting user layer (in case we're not using drop shadow) progress_log('Restoring user layer selection...') try: pdb.gimp_image_set_active_layer(the_img,user_layer) except BaseException, error: error_log('Calling gimp_image_set_active_layer to stored user layer: error: ' + str(error)) # 8. Call Filters > Light and Shadow > Drop Shadow if not add_dropshadow: debug_log('Parameter passed from UI said not to add drop shadow') else: progress_log('Adding drop shadow...') try: pdb.script_fu_drop_shadow(the_img, the_drawable, ds_offset_x, ds_offset_y, ds_blur_radius, ds_color, ds_opacity, allow_resize) except BaseException, error: error_log('Calling script_fu_drop_shadow: error: ' + str(error)) # 8b. Find and store 'Drop Shadow' layer progress_log('Tagging drop shadow layer...') ds_layer_found = False for ds_layer in the_img.layers: if ds_layer.name == "Drop Shadow": ds_layer_found = True break if not ds_layer_found: error_log("No layer named 'Drop Shadow' found") # 8c. set ds_layer and user_layer linked try: pdb.gimp_drawable_set_linked(ds_layer, True) except BaseException, error: error_log('Linking drop shadow layer: error: ' + str(error)) try: pdb.gimp_drawable_set_linked(user_layer, True) except BaseException, error: error_log('Linking user layer: error: ' + str(error)) # 8d. Move selection to drop shadow progress_log('Translating selection to drop shadow location...') try: pdb.gimp_selection_translate(the_img, ds_offset_x, ds_offset_y) except BaseException, error: error_log('Calling gimp_selection_translate to drop shadow: error: ' + str(error)) # 8e. Extend selection by extra area added by gaussian blur if ds_blur_radius > 0 and crop and crp_tightness < 10: progress_log('Growing selection to include drop shadow blur...') grow_radius = int(ds_blur_radius * (10 - crp_tightness) * (10 - crp_tightness) * (10 - crp_tightness) / 1000) try: pdb.gimp_selection_grow(the_img, grow_radius) except BaseException, error: error_log('Calling gimp_selection_grow to include drop shadow blur: error: ' + str(error)) # 8f. Combine original (or bordered) selection with translated selection progress_log('Combining drop shadow & main selection...') try: pdb.gimp_selection_combine(saved_selection_channel, 0) except BaseException, error: error_log('Calling gimp_selection_combine adding drop shadow: error: ' + str(error)) # 8g. Resizing border_layer to match possibly modified image size if add_border: progress_log('Resizing border layer to match shadowed image...') try: pdb.gimp_layer_resize_to_image_size(bdr_layer) except BaseException, error: error_log('Calling gimp_layer_resize_to_image_size on border layer: error: ' + str(error)) # 8h. Bumping border layer up above shadow progress_log('Raising border layer above drop shadow...') try: pdb.gimp_image_raise_layer(the_img,bdr_layer) except BaseException, error: error_log('Calling gimp_image_raise_layer on border layer: error: ' + str(error)) # 8i. Bumping user_layer up above shadow (unless we're merging later) if merge_layers: debug_log('Not raising user layer in deference to later merge') else: progress_log('Raising user layer above drop shadow...') try: pdb.gimp_image_raise_layer(the_img,user_layer) except BaseException, error: error_log('Calling gimp_image_raise_layer on user layer') # 8j. Resizing user_layer to match possibly modified image size progress_log('Resizing user layer to match shadowed image...') try: pdb.gimp_layer_resize_to_image_size(user_layer) except BaseException, error: error_log('Calling gimp_layer_resize_to_image_size on stored user layer: error: ' + str(error)) # 9. Manually crop image if user has chosen to do so. # (Doing an auto-crop seems to eat up too much into the shadow...) if not crop: debug_log('User-supplied UI param said not to crop image.') else: progress_log('Cropping image...') try: #Due to previous operations, we're sure we have a selection... has_sel, x1, y1, x2, y2 = pdb.gimp_selection_bounds(the_img) pdb.gimp_image_crop(the_img, x2-x1, y2-y1, x1, y1) except BaseException, error: error_log('Cropping image: error: ' + str(error)) # 9b. Done with these saved selections progress_log('Removing stored selection channels...') try: pdb.gimp_image_remove_channel(the_img, saved_selection_channel) except: error_log('Removing saved_selection_channel: error: ' + str(error)) if add_border: try: pdb.gimp_image_remove_channel(the_img, border_selection_channel) except: error_log('Removing border_selection_channel: error: ' + str(error)) try: pdb.gimp_image_remove_channel(the_img, saved_border_selection_channel) except: error_log('Removing saved_border_selection_channel: error: ' + str(error)) # 9c. Merge visible layers if not merge_layers: debug_log('User-supplied UI param said not to merge layers.') else: progress_log('Merging layers...') if add_dropshadow: progress_log('Merging drop shadow layer down...') try: pdb.gimp_image_merge_down(the_img, ds_layer, 0) except BaseException, error: error_log('Merging drop shadow layer down...: error: ' + str(error)) if add_border: progress_log('Merging border layer to background...') try: pdb.gimp_image_merge_down(the_img, bdr_layer, 0) except BaseException, error: error_log('Merging border layer to background...: error: ' + str(error)) # 9d. End the undo group we started in step 1b. progress_log('Closing undo group...') try: pdb.gimp_image_undo_group_end(the_img) except BaseException, error: error_log('Calling pdb.gimp_image_undo_group_end: error: ' + str(error)) #Register the plugin with GIMP. #For help/more info on params, read http://www.gimp.org/docs/python/index.html #Construct our list of params for register() in a verbose manner... params_list = [] params_list.append( (PF_IMAGE, "image", "Input image", None) ) params_list.append( (PF_DRAWABLE, "drawable", "Input drawable", None) ) #Params that we will pass to the selection-distort procedure: params_list.append( (PF_SPINNER, "distort_threshold", "(Selection) Distort\n\t- Threshold", 111, (1,255,1)) ) params_list.append( (PF_SPINNER, "distort_spread", "\t- Spread", 10, (0,1000,1)) ) params_list.append( (PF_SPINNER, "distort_granularity", "\t- Granularity (1 is low)", 4, (1,25,1)) ) params_list.append( (PF_SPINNER, "distort_smooth_level", "\t- Smoothing Level", 5, (1,150,1)) ) #Parameters specific to this plugin: params_list.append( (PF_BOOL, "add_alpha", "Delete to transparency?\n (add an alpha channel to image)", True) ) params_list.append( (PF_TOGGLE, "allow_resize", "Allow resizing?", True) ) params_list.append( (PF_BOOL, "add_dropshadow", "Add drop shadow?", True) ) #Parameters for the drop shadow (if user chooses to apply it): params_list.append( (PF_SPINNER, "ds_offset_x", "\t- X axis offset (pixels)", 8, (-25,25,1)) ) params_list.append( (PF_SPINNER, "ds_offset_y", "\t- Y axis offset (pixels)", 8, (-25,25,1)) ) params_list.append( (PF_SPINNER, "ds_blur_radius", "\t- Blur Radius", 15, (0,1024,1)) ) params_list.append( (PF_COLOR, "ds_color", "\t- Shadow Color", (12,12,12)) ) params_list.append( (PF_SLIDER, "ds_opacity", "\t- Shadow Opacity", 80, (0,100,1)) ) #Another parameter specific to this plugin: params_list.append( (PF_BOOL, "add_border", "Add border?", True) ) #Parameters for the border (if user chooses to apply it): params_list.append( (PF_SPINNER, "bdr_offset", "\t- Size (pixels)", 2, (0,25,1)) ) params_list.append( (PF_COLOR, "bdr_color", "\t- Color", (0,0,0)) ) params_list.append( (PF_OPTION, "bdr_edge", "\t- Edge", 0, ["Feathered","Hard"]) ) #More parameter specific to this plugin: params_list.append( (PF_TOGGLE, "crop", "Crop image down to selection?\n (plus any drop shadow/border)", True) ) params_list.append( (PF_SLIDER, "crp_tightness", "\t- How close?\n\t (10 is tightest)", 4, (0,10,1)) ) params_list.append( (PF_TOGGLE, "merge_layers", "Merge layers upon completion?", True) ) #First invocation to log() in this script's run; empty out the log file. global log_opened log_opened = False debug_log("Initiating plugin") debug_log("About to register") register( "python-screenshot-tearoff", "Applies an irregular 'tear-off' effect to the image at any selection edge that is NOT at the boundary of the image; deletes the remainder of the image, optionally applies a drop shadow to the 'to-keep' area, and optionally crops the image.", "Select the part of the image that you want to keep, before invoking this plugin. You can adjust the selection distort/distress parameters, and choose whether to apply a drop shadow and whether to crop the image after that.", "Edgar D'Souza (edgar.b.dsouza@gmail.com)", "(c) 2008 Edgar D'Souza, licensed under GPL v3 or later", "2008-10-26", N_("_Tear-off..."), "RGB*, GRAY*", params_list, [], tearoff, menu="/Filters/Distorts", domain=("gimp20-python", gimp.locale_directory) ) debug_log("Registration finished") debug_log("Calling main() to start the plugin running") main()