├── .gitignore ├── COPYING ├── README ├── gimp2 ├── arclayer.py ├── arclayer.scm ├── arrowdesigner.py ├── blobi.py ├── changefont.py ├── cutout.py ├── export-all.py ├── export-scaled.py ├── fibonacci.scm ├── fontasia-gimp.py ├── gimplabels │ ├── ChangeLog │ ├── Makefile │ ├── README │ ├── labels.scm │ ├── labels.scm-dist │ ├── labeltemplates.scm │ ├── labeltemplates.scm-dist │ └── make-label-fu.py ├── life.py ├── migrate-gimp-presets.py ├── migrate-plugins-2-99.py ├── pandora-combine.scm ├── pyui.py ├── saver.py ├── sf-helloworld.scm ├── stack.scm ├── text-along-path.py └── wallpaper.py └── gimp3 ├── README.md ├── arrowdesigner.py ├── blobi.py ├── gimphelpers.py ├── pandora.py ├── saver.py └── wallpaper.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.py[co] 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | A collection of GIMP scripts and plug-ins by Akkana Peck. 2 | 3 | These are all licensed under the GPL v2 or (at your option) any later GPL. 4 | There's further discussion of some of them on my software page: 5 | http://www.shallowsky.com/software/#gimp 6 | 7 | Python plug-ins in the gimp2 directory are for the classic GIMP 2.x API 8 | (which also requires that you have python2 and pygtk installed). 9 | GIMP 3.0 is now out, and its Python API has changed completely. 10 | (This is actually a good thing, despite the short-term irritation of 11 | having to rewrite everything.) 12 | I'm gradually migrating Python plug-ins to GIMP 3, putting them in the 13 | gimp3 directory. If you need a plug-in that hasn't been updated yet, 14 | email me or file an issue on GitHub and I'll get to it as soon as I can. 15 | 16 | Script-fu scripts (ending in .scm) mostly haven't been tested yet in GIMP3. 17 | I'm not sure if they work or not. If you need one of the script-fus 18 | updated, again, email me or file a GitHub issue. 19 | 20 | =============== 21 | INSTALLING PYTHON PLUG-INS: 22 | 23 | First, find your plug-ins directory. Edit->Preferences, then scroll 24 | down to Folders and expand it, scroll down again and click on Plug-ins. 25 | You can choose any of those folders, but most typically you'd pick the one 26 | that's in your user home directory, unless you're installing it for 27 | multiple users on the same system. 28 | 29 | Linux and Mac users will also have to make sure the plug-in is executable: 30 | chmod +x filename.py 31 | 32 | In GIMP 2.x, install a plug-in file by copying filename.py into 33 | your plug-ins directory. 34 | 35 | In GIMP 3.x, make a directory named the same as the filename, but 36 | without the .py, then copy the .py file into it. For instance, to install 37 | saver.py on Linux for GIMP 3.0: 38 | mkdir ~/.config/GIMP/3.0/plug-ins/saver 39 | cp saver.py ~/.config/GIMP/3.0/plug-ins/saver 40 | 41 | =============== 42 | FAVORITE PLUG-INS (PYTHON): 43 | 44 | (These are the plug-ins ported to GIMP 3 so far.) 45 | 46 | saver.py 47 | (File->Saver and File->Saver as) 48 | Meant to be the One True Save/Export plug-in, which you can 49 | (if you choose) bind to ctrl-S and ctrl-Shift-S. 50 | It saves whether the type is XCF or other, it can automatically save 51 | an XCF plus a copy in a different format, and it can even scale that 52 | copy and remember the scaling. 53 | More info: http://www.shallowsky.com/software/gimp-save/ 54 | 55 | arrowdesigner.py 56 | (Filters->Render->Arrow Designer) 57 | Interactively draw an arrow in a new layer, using a rectangular 58 | selection as a guide. 59 | 60 | pandora.py 61 | (Filters->Combine->Pandora panorama) 62 | Stitch a series of images (loaded as layers in one image) into a panorama. 63 | Doesn't do any smart matching based on the image contents; it just gives 64 | you initial fixed spacing and layer masks; then you can drag the images 65 | around to choose final placement, and edit the layer masks as needed. 66 | More info: http://www.shallowsky.com/software/pandora/ 67 | 68 | wallpaper.py 69 | (Image->Selection to Wallpaper) 70 | Make desktop wallpaper from the current image and selection. 71 | It has a few screen size presets (e.g. 1920x1200, 1920x1080, 72 | 1680x1050, 1366x768, 1080x2400), and guesses which one to use 73 | based on the spect ratio of the current selection. 74 | It makes a new image containing only what's in the selection, 75 | scales it to the right size, then 76 | saves to a known place, defaulting to ~/Images/Backgrounds/1920x1200. 77 | 78 | It's recommended that you set rect select presets for aspect ratios 79 | like 16:8, 4:3, 1366:768, etc. if you make wallpapers often. 80 | 81 | blobi.py 82 | Simple text effect similar to bevel and drop shadow, but more "blobby". 83 | 84 | =============== 85 | OLDER PYTHON2 PLUG-INS: 86 | 87 | (These have not been ported to GIMP 3. If you need one of them ported, 88 | please file an issue or email me.) 89 | 90 | arclayer.py 91 | Bend a layer (e.g. a text layer) in a circular arc. 92 | Also a good demo of how to use pixel regions in Python. 93 | 94 | changefont.py 95 | Change font face and size for all text layers in the current image. 96 | 97 | export-scaled.py 98 | Export a scaled version of the current image, remembering name & scale 99 | for the next export. 100 | 101 | export-all.py 102 | Show a dialog offering to export all open images. 103 | Currently this is just a skeleton for other people to build on. 104 | 105 | life.py 106 | Conway's Game of Life (more or less, with colors added). 107 | No, there's no practical use for it. It's just a silly demo. 108 | 109 | migrate-gimp-presets.py 110 | Migrate 2.6 tool presets to 2.8 (which doesn't happen automatically). 111 | 112 | pyui.py 113 | Demonstrate all possible UI options for pygimp registration dialogs. 114 | 115 | =============== 116 | SCRIPT-FU: 117 | 118 | (None of these have been ported to GIMP 3.) 119 | 120 | arclayer.scm 121 | Bend a layer (e.g. a text layer) in a circular arc. 122 | Much much slower than the Python version, due to lack of pixel regions. 123 | Nore info: http://www.shallowsky.com/software/arclayer/ 124 | 125 | fibonacci.scm 126 | Draw a Fibonacci (golden ratio) spiral around Fibonacci number boxes. 127 | 128 | gimplabels/ 129 | Create label/businesscard templates, and pages of them, based on 130 | Avery and similar templates imported from glabels. 131 | Fiddly to use since you have to figure out your printer's "slop factor". 132 | More info: http://www.shallowsky.com/software/gimplabels/ 133 | 134 | pandora-combine.scm 135 | Stitch a series of images (loaded as layers in one image) into a panorama. 136 | Doesn't do any smart matching based on the image contents; it just gives 137 | you initial fixed spacing and layer masks; then you can drag the images 138 | around to choose final placement, and edit the layer masks as needed. 139 | More info: http://www.shallowsky.com/software/pandora/ 140 | 141 | stack.scm 142 | Make an averaged image stack: 143 | combine all the layers with opacity 1/num_images. 144 | 145 | sf-helloworld.scm 146 | A simple script-fu demonstration (used in GIMP scripting talks). 147 | -------------------------------------------------------------------------------- /gimp2/arclayer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # arclayer.py: Gimp plug-in to warp one layer along an arc of a given radius. 4 | 5 | # Copyright 2002,2008 by Akkana Peck, http://www.shallowsky.com/software/ 6 | # You may use and distribute this plug-in under the terms of the GPL. 7 | # 8 | # Thanks to Joao Bueno for some excellent tips on speeding up 9 | # pixel ops in gimp-python! 10 | 11 | import math 12 | import time 13 | from gimpfu import * 14 | from array import array 15 | 16 | def python_arc_layer(img, layer, radius, ontop) : 17 | gimp.progress_init("Arcing layer" + layer.name + "...") 18 | 19 | # Spinner passes an integer, which will upset later calculations 20 | radius = float(radius) 21 | 22 | pdb.gimp_image_undo_group_start(img) 23 | 24 | layername = "arc " + layer.name 25 | # Calculate the size for the new layer 26 | theta2 = layer.width / radius / 2 27 | newWidth = int(2 * radius * math.sin(theta2)) 28 | if theta2 <= 1.570795 : # PI/2: if we're doing less than a semicircle 29 | newHeight = int(layer.height * math.cos(theta2) 30 | + .5 * radius * math.sin(theta2) * 31 | math.tan(theta2)) 32 | else : 33 | newHeight = layer.height + int(radius * (1 - math.cos(theta2))) 34 | 35 | #print "Old size: ",layer.width,"x",layer.height 36 | #print "New size: ",newWidth,"x",newHeight 37 | #print "r =", radius, ", theta/2 =", theta2 38 | 39 | # Create the new layer: 40 | destDrawable = gimp.Layer(img, layername, newWidth, newHeight, 41 | layer.type, layer.opacity, layer.mode) 42 | img.add_layer(destDrawable, 0) 43 | xoff, yoff = layer.offsets 44 | destDrawable.translate(xoff - (newWidth - layer.width)/2, 45 | yoff - (newHeight - layer.height)/2) 46 | 47 | # No need to clear the layer here -- 48 | # we'll initialize it to zero when we create the pixel array. 49 | 50 | srcWidth, srcHeight = layer.width, layer.height 51 | srcRgn = layer.get_pixel_rgn(0, 0, srcWidth, srcHeight, 52 | False, False) 53 | src_pixels = array("B", srcRgn[0:srcWidth, 0:srcHeight]) 54 | 55 | dstRgn = destDrawable.get_pixel_rgn(0, 0, newWidth, newHeight, 56 | True, True) 57 | p_size = len(srcRgn[0,0]) 58 | dest_pixels = array("B", "\x00" * (newWidth * newHeight * p_size)) 59 | 60 | # Finally, loop over the region: 61 | for x in xrange(0, srcWidth - 1) : 62 | for y in xrange(0, srcHeight) : 63 | # Calculate new coordinates 64 | phi = theta2 - x/radius 65 | if ontop : 66 | r = radius - y 67 | newy = int(radius - r * math.cos(phi)) 68 | if (newy < 0) : 69 | continue 70 | else : 71 | r = radius - layer.height + y 72 | newy = newHeight \ 73 | + int(r * math.cos(phi) - radius) 74 | if (newy > newHeight) : 75 | continue 76 | newx = int(newWidth/2 - r * math.sin(phi)) 77 | 78 | src_pos = (x + srcWidth * y) * p_size 79 | dest_pos = (newx + newWidth * newy) * p_size 80 | 81 | newval = src_pixels[src_pos: src_pos + p_size] 82 | dest_pixels[dest_pos : dest_pos + p_size] = newval 83 | 84 | # A fast (but not great) cheat to fill in holes: 85 | # Write to two pixel above the new location 86 | # and one pixel to the right of it. 87 | # If that's a valid location, then later 88 | # another set of coordinates will map to it, 89 | # and overwrite this value; if not, then it 90 | # would have been a hole, and this fills it. 91 | if (newy < newHeight-1) : 92 | next_row = dest_pos + p_size * newWidth 93 | dest_pixels [next_row : next_row + p_size] \ 94 | = newval 95 | if (newx < newWidth-1) : 96 | next_col = dest_pos + p_size 97 | dest_pixels [next_col : next_col + p_size] \ 98 | = newval 99 | 100 | #print "Progress:", 100.0 * x / layer.width 101 | # Docs say progress_update takes a percent, 102 | # but it's really a fraction. 103 | # GIMP whines "arclayer.py is updating the progress too often" 104 | # but, well, tough. It's only ten times total. 105 | progress = float(x)/layer.width 106 | if (int(progress * 100) % 20 == 0) : 107 | gimp.progress_update(progress) 108 | 109 | # Copy the whole array back to the pixel region: 110 | dstRgn[0:newWidth, 0:newHeight] = dest_pixels.tostring() 111 | 112 | destDrawable.flush() 113 | destDrawable.merge_shadow(True) 114 | destDrawable.update(0, 0, newWidth,newHeight) 115 | 116 | # Remove the old layer 117 | #img.remove_layer(layer) 118 | layer.visible = False 119 | 120 | pdb.gimp_selection_none(img) 121 | pdb.gimp_image_undo_group_end(img) 122 | 123 | register( 124 | "python_fu_arc_layer", 125 | "Bend a layer in an arc", 126 | "Bend a layer in an arc", 127 | "Akkana Peck", 128 | "Akkana Peck", 129 | "2002,2008", 130 | "/Filters/Distorts/ArcLayer(py)...", 131 | "*", 132 | [ 133 | (PF_SPINNER, "Radius", "Arc Radius (pixels)", 134 | 400, (0, 2000, 50)), 135 | (PF_TOGGLE, "Top", "Top of circle?", 1), 136 | ], 137 | [], 138 | python_arc_layer) 139 | 140 | main() 141 | -------------------------------------------------------------------------------- /gimp2/arclayer.scm: -------------------------------------------------------------------------------- 1 | ; arclayer.scm: a very simple version of arclayer 2 | ; that just calls Polar Coordinates. 3 | ; See http://shallowsky.com/software/arclayer/ 4 | ; Copyright (C) 2009 by Akkana Peck, akkana@shallowsky.com. 5 | ; 6 | ; This program is free software; you can redistribute it and/or modify 7 | ; it under the terms of the GNU General Public License as published by 8 | ; the Free Software Foundation; either version 2 of the License, or 9 | ; (at your option) any later version. 10 | ; 11 | ; This program is distributed in the hope that it will be useful, 12 | ; but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ; GNU General Public License for more details. 15 | ; 16 | ; You should have received a copy of the GNU General Public License 17 | ; along with this program; if not, write to the Free Software 18 | ; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 19 | 20 | ; pass a box 200x100 to polar coordinates 21 | ; get a circle of diameter 106 (at least, that's the layer height) 22 | 23 | 24 | (define (script-fu-arclayer img layer radius top) 25 | (gimp-image-undo-group-start img) 26 | 27 | (if (= top FALSE) (plug-in-rotate RUN-NONINTERACTIVE img layer 2 FALSE)) 28 | (let* ((oldwidth (car (gimp-drawable-width layer))) 29 | (oldheight (car (gimp-drawable-height layer))) 30 | (newwidth (* 6.28318 radius)) 31 | ;(newwidth (* 3.14 radius)) 32 | (xoff (/ (- newwidth oldwidth) 2)) 33 | ) 34 | (gimp-layer-resize layer newwidth radius xoff 0) 35 | ) 36 | 37 | (plug-in-polar-coords RUN-NONINTERACTIVE img layer 38 | 100 180 FALSE FALSE TRUE) 39 | (if (= top FALSE) (plug-in-rotate RUN-NONINTERACTIVE img layer 2 FALSE)) 40 | 41 | (plug-in-autocrop-layer RUN-NONINTERACTIVE img layer) 42 | 43 | (gimp-image-undo-group-end img) 44 | (gimp-displays-flush) 45 | ) 46 | 47 | (script-fu-register "script-fu-arclayer" 48 | _"/Filters/Distorts/Arclayer (SF)..." 49 | "Bend the current layer in an arc." 50 | "Akkana Peck" 51 | "Akkana Peck" 52 | "March 2009" 53 | "*" 54 | SF-IMAGE "Image" 0 55 | SF-DRAWABLE "Drawable" 0 56 | SF-ADJUSTMENT "Radius" '(600 1 2000 10 50 0 1) 57 | SF-TOGGLE "Top" TRUE) 58 | 59 | 60 | -------------------------------------------------------------------------------- /gimp2/arrowdesigner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Draw arrows in GIMP, using the selection as a guide for where to draw. 5 | # 6 | # Copyright 2010,2011 by Akkana Peck, http://www.shallowsky.com/software/ 7 | # plus contributions from Robert Brizard. 8 | # You may use and distribute this plug-in under the terms of the GPL. 9 | 10 | from gimpfu import * 11 | import gtk, pango 12 | from gobject import timeout_add 13 | 14 | # Direction "enums" 15 | DIREC_N, DIREC_NE, DIREC_E, DIREC_SE, DIREC_S, DIREC_SW, DIREC_W, DIREC_NW \ 16 | = range(8) 17 | 18 | def python_fu_arrow_from_selection(img, layer, arrowangle, arrowsize, 19 | x1, y1, x2, y2, cyc) : 20 | """ 21 | Draw an arrow from (x1, y1) to (x2, y2) with the head at (x2, y2). 22 | The size of the arrowhead (isocele triangle and arrowsize is the length of the isoside) 23 | is controlled by 'arrowsize', and the angle of it by 'arrowangle'. 24 | 'cyc' is the wanted integer number of gradient length in the shaft. 25 | """ 26 | # Save the current selection: 27 | savesel = pdb.gimp_selection_save(img) 28 | pdb.gimp_selection_none(img) 29 | 30 | aangle = arrowangle * math.pi / 180. 31 | 32 | # 33 | # Draw the line first. 34 | # But don't go quite all the way to the end, because that 35 | # would make a rounded tip where the arrow point should be. 36 | # 37 | strokes = [ x1, y1, x2, y2 ] 38 | dy = y2 - y1 39 | dx = x2 - x1 40 | # length of arrowhead in the shaft direction 41 | l_head = arrowsize * math.cos(aangle) 42 | 43 | l_arrow = math.sqrt(dx*dx + dy*dy) 44 | # ratio is length_head/length_arrow, if >= 1 no line 45 | ratio = l_head / l_arrow * 0.5 46 | if ratio < 1.0 : 47 | # from similar triangles 48 | strokes[2] -= int(round(ratio*dx)) 49 | strokes[3] -= int(round(ratio*dy)) 50 | 51 | # compute the length of the gradient cycle wanted 52 | if cyc > 0: cycl_grad = int((l_arrow - l_head)/cyc) 53 | elif cyc == 0: cycl_grad = 0 54 | 55 | pdb.gimp_paintbrush(layer, 0, 4, strokes, 0, cycl_grad) 56 | 57 | # 58 | # Now make the arrowhead 59 | # 60 | theta = math.atan2(y2-y1, x2-x1) 61 | points = [ x2, y2, 62 | int(x2 - arrowsize * math.cos(theta - aangle)), 63 | int(y2 - arrowsize * math.sin(theta - aangle)), 64 | int(x2 - arrowsize * math.cos(theta + aangle)), 65 | int(y2 - arrowsize * math.sin(theta + aangle)) ] 66 | # Only draw the head if the 3 points aren't collinear. 67 | # Otherwise, it can fill the whole arrow layer: 68 | # e.g. try arrow size 1, arrow angle < 30. 69 | if int(l_head) > 1 and points[2:4] != points[4:6] : 70 | # Select the arrowhead shape 71 | pdb.gimp_image_select_polygon(img, CHANNEL_OP_REPLACE, 6, points) 72 | # Fill the arrowhead 73 | pdb.gimp_edit_fill(layer, FILL_FOREGROUND) 74 | 75 | # Restore the old selection 76 | pdb.gimp_image_select_item(img, CHANNEL_OP_REPLACE, savesel) 77 | 78 | def direc_to_coords(x1, y1, x2, y2, direction) : 79 | if direction == DIREC_N : 80 | return x1, y2, x1, y1 81 | elif direction == DIREC_NE : 82 | return x1, y2, x2, y1 83 | elif direction == DIREC_E : 84 | return x1, y1, x2, y1 85 | elif direction == DIREC_SE : 86 | return x1, y1, x2, y2 87 | elif direction == DIREC_S : 88 | return x1, y1, x1, y2 89 | elif direction == DIREC_SW : 90 | return x2, y1, x1, y2 91 | elif direction == DIREC_W : 92 | return x2, y1, x1, y1 93 | elif direction == DIREC_NW : 94 | return x2, y2, x1, y1 95 | 96 | class ArrowWindow(gtk.Window): 97 | def __init__ (self, img, *args): 98 | self.img = img 99 | self.x1, self.y1, self.x2, self.y2 = 0, 0, 0, 0 100 | 101 | self.direction = DIREC_N 102 | self.changed = False 103 | self.arrowsize = 30 104 | self.arrowangle = 25 105 | self.num_grad = 0 106 | 107 | # Make a new GIMP layer to draw on 108 | self.layer = gimp.Layer(img, "arrow", img.width, img.height, 109 | RGBA_IMAGE, 100, NORMAL_MODE) 110 | img.add_layer(self.layer, 0) 111 | 112 | # Create the dialog 113 | win = gtk.Window.__init__(self, *args) 114 | self.set_title("GIMP arrow tool") 115 | 116 | # Mac may have a problem with the window disappearing below 117 | # the image window. But on small screens, the window is big 118 | # enough that it can block a lot of the image window. 119 | # Ideally, it would be nice to make sure it's initially 120 | # on top, but then let the user hide it later. 121 | # Or make a checkbox for it in the dialog, but that would 122 | # make the dialog even bigger. 123 | #self.set_keep_above(True) # keep the window on top 124 | 125 | # Obey the window manager quit signal: 126 | self.connect("destroy", gtk.main_quit) 127 | 128 | # Make the UI 129 | self.set_border_width(10) 130 | vbox = gtk.VBox(spacing=10, homogeneous=False) 131 | self.add(vbox) 132 | label = gtk.Label("Arrow designer by Akkana Peck\nMake a rectangular selection. Will use active colors, brush and gradient.") 133 | 134 | # Change color of the label first line R. B. 135 | attr = pango.AttrList() 136 | fg_color = pango.AttrForeground(0, 0, 65535, 0, 30) 137 | size = pango.AttrSize(17000, 0, 14) 138 | bold = pango.AttrWeight(pango.WEIGHT_ULTRABOLD, 0, 14) 139 | attr.insert(fg_color) 140 | attr.insert(size) 141 | attr.insert(bold) 142 | label.set_attributes(attr) 143 | 144 | vbox.add(label) 145 | label.show() 146 | 147 | table = gtk.Table(rows=3, columns=2, homogeneous=False) 148 | table.set_col_spacings(10) 149 | vbox.add(table) 150 | 151 | # Arrow size and sharpness 152 | label = gtk.Label("Arrowhead size (px)") 153 | label.set_alignment(xalign=0.0, yalign=1.0) 154 | table.attach(label, 0, 1, 0, 1, xoptions=gtk.FILL, yoptions=0) 155 | label.show() 156 | adj = gtk.Adjustment(self.arrowsize, 1, 400, 1) 157 | adj.connect("value_changed", self.arrowsize_cb) 158 | scale = gtk.HScale(adj) 159 | scale.set_digits(0) 160 | table.attach(scale, 1, 2, 0, 1) 161 | scale.show() 162 | 163 | label = gtk.Label("Arrowhead angle (°)") 164 | label.set_alignment(xalign=0.0, yalign=1.0) 165 | table.attach(label, 0, 1, 1, 2, xoptions=gtk.FILL, yoptions=0) 166 | label.show() 167 | adj = gtk.Adjustment(self.arrowangle, 1, 80, 1) 168 | adj.connect("value_changed", self.arrowangle_cb) 169 | scale = gtk.HScale(adj) 170 | scale.set_digits(0) 171 | table.attach(scale, 1, 2, 1, 2) 172 | scale.show() 173 | 174 | label = gtk.Label("Gradient repetitions") 175 | label.set_alignment(xalign=0.0, yalign=1.0) 176 | table.attach(label, 0, 1, 2, 3, xoptions=gtk.FILL, yoptions=0) 177 | label.show() 178 | adj = gtk.Adjustment(self.num_grad, 0, 50, 1) 179 | adj.connect("value_changed", self.num_grad_cb) 180 | scale = gtk.HScale(adj) 181 | scale.set_digits(0) 182 | table.attach(scale, 1, 2, 2, 3) 183 | scale.show() 184 | 185 | table.show() 186 | 187 | # Selector for arrow direction 188 | hbox = gtk.HBox(spacing=5) 189 | 190 | btn = gtk.RadioButton(None, "N") 191 | btn.set_active(True) 192 | btn.connect("toggled", self.direction_cb, DIREC_N) 193 | hbox.add(btn) 194 | btn.show() 195 | btn = gtk.RadioButton(btn, "NE") 196 | btn.connect("toggled", self.direction_cb, DIREC_NE) 197 | hbox.add(btn) 198 | btn.show() 199 | btn = gtk.RadioButton(btn, "E") 200 | btn.connect("toggled", self.direction_cb, DIREC_E) 201 | hbox.add(btn) 202 | btn.show() 203 | btn = gtk.RadioButton(btn, "SE") 204 | btn.connect("toggled", self.direction_cb, DIREC_SE) 205 | hbox.add(btn) 206 | btn.show() 207 | btn = gtk.RadioButton(btn, "S") 208 | btn.connect("toggled", self.direction_cb, DIREC_S) 209 | hbox.add(btn) 210 | btn.show() 211 | btn = gtk.RadioButton(btn, "SW") 212 | btn.connect("toggled", self.direction_cb, DIREC_SW) 213 | hbox.add(btn) 214 | btn.show() 215 | btn = gtk.RadioButton(btn, "W") 216 | btn.connect("toggled", self.direction_cb, DIREC_W) 217 | hbox.add(btn) 218 | btn.show() 219 | btn = gtk.RadioButton(btn, "NW") 220 | btn.connect("toggled", self.direction_cb, DIREC_NW) 221 | hbox.add(btn) 222 | btn.show() 223 | 224 | vbox.add(hbox) 225 | hbox.show() 226 | 227 | # Make the dialog buttons box 228 | hbox = gtk.HBox(spacing=20) 229 | 230 | btn = gtk.Button("Next arrow") 231 | btn.connect("pressed", self.next_arrow) 232 | hbox.add(btn) 233 | btn.show() 234 | 235 | btn = gtk.Button("Close") 236 | btn.connect("clicked", self.close_window) 237 | hbox.add(btn) 238 | btn.show() 239 | 240 | vbox.add(hbox) 241 | hbox.show() 242 | vbox.show() 243 | self.show() 244 | 245 | timeout_add(300, self.update, self) 246 | 247 | return win 248 | 249 | def close_window(self, widget) : 250 | # Autocrop our new layer before closing. 251 | # autocrop_layer crops the current layer using the layer 252 | # passed in as its crop template -- not clear from the doc. 253 | self.img.active_layer = self.layer 254 | pdb.plug_in_autocrop_layer(self.img, self.layer) 255 | gtk.main_quit() 256 | 257 | def direction_cb(self, widget, data=None) : 258 | self.direction = data 259 | self.changed = True 260 | 261 | def arrowsize_cb(self, val) : 262 | self.arrowsize = val.value 263 | self.changed = True 264 | 265 | def arrowangle_cb(self, val) : 266 | self.arrowangle = val.value 267 | self.changed = True 268 | 269 | def num_grad_cb(self, val) : 270 | self.num_grad = val.value 271 | self.changed = True 272 | 273 | def arrow(self, x1, y1, x2, y2) : 274 | python_fu_arrow_from_selection(self.img, self.layer, 275 | self.arrowangle, self.arrowsize, 276 | x1, y1, x2, y2, self.num_grad) 277 | 278 | def update(self, *args): 279 | exists, x1, y1, x2, y2 = pdb.gimp_selection_bounds(self.img) 280 | timeout_add(500, self.update, self) 281 | if not exists : 282 | return # No selection, no arrow 283 | if (self.x1, self.y1, self.x2, self.y2) == (x1, y1, x2, y2) \ 284 | and not self.changed : 285 | return 286 | self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2 287 | self.changed = False 288 | 289 | # Clear the layer, erasing the old arrow 290 | # pdb.gimp_edit_clear(self.layer) 291 | # self.layer.fill(TRANSPARENT_FILL) 292 | 293 | self.layer.fill(FILL_TRANSPARENT) 294 | 295 | # Draw the new arrow. 296 | # Order is from, to: arrowhead goes on second X, Y pair. 297 | x1, y1, x2, y2 = direc_to_coords(x1, y1, x2, y2, self.direction) 298 | self.arrow(x1, y1, x2, y2) 299 | 300 | pdb.gimp_displays_flush() 301 | 302 | def next_arrow(self, data=None): 303 | # pdb.gimp_selection_none(self.img) 304 | # Make a new GIMP layer to draw on 305 | self.layer = gimp.Layer(self.img, "arrow", self.img.width, self.img.height, 306 | RGBA_IMAGE, 100, NORMAL_MODE) 307 | self.img.add_layer(self.layer, 0) 308 | pdb.gimp_displays_flush() 309 | 310 | def arrow_designer(image, layer): 311 | image.undo_group_start() 312 | 313 | r = ArrowWindow(image) 314 | gtk.main() 315 | 316 | # pdb.gimp_selection_none(image) 317 | image.undo_group_end() 318 | 319 | def arrow_from_selection(img, layer, angle, size, direction) : 320 | exists, x1, y1, x2, y2 = pdb.gimp_selection_bounds(img) 321 | if not exists : 322 | return 323 | 324 | x1, y1, x2, y2 = direc_to_coords(x1, y1, x2, y2, direction) 325 | 326 | python_fu_arrow_from_selection(img, layer, angle, size, x1, y1, x2, y2, 0) 327 | 328 | register( 329 | "python_fu_arrow_interactive", 330 | "Draw an arrow following the selection (interactive).", 331 | # + "From: " + str(__file__), 332 | "Draw an arrow following the current selection, updating as the selection changes", 333 | "Akkana Peck", "Akkana Peck", 334 | "2010,2011", 335 | "/Filters/Render/Arrow designer...", 336 | "*", 337 | [ 338 | ], 339 | [], 340 | arrow_designer) 341 | 342 | register( 343 | "python_fu_arrow_from_selection", 344 | "Draw an arrow following the current selection", 345 | "Draw an arrow following the current selection", 346 | "Akkana Peck", "Akkana Peck", 347 | "2010,2011", 348 | "/Filters/Render/Arrow from selection...", 349 | "*", 350 | [ 351 | (PF_SLIDER, "angle", "Arrow angle", 30, (0, 200, 1)), 352 | (PF_SLIDER, "size", "Arrow size", 25, (0, 80, 1)), 353 | (PF_OPTION, "direction", "Direction", 2, 354 | ("N", "NE", "E", "SE", "S", "SW", "W", "NW")), 355 | ], 356 | [], 357 | arrow_from_selection) 358 | 359 | main() 360 | -------------------------------------------------------------------------------- /gimp2/blobi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from gimpfu import * 4 | 5 | def python_blobify(img, layer, blur) : 6 | pdb.gimp_undo_push_group_start(img) 7 | 8 | # Alpha to selection: 9 | pdb.gimp_selection_layer_alpha(layer) 10 | # Invert the selection: 11 | pdb.gimp_selection_invert(img) 12 | # Make a dropshadow from the inverted selection 13 | pdb.script_fu_drop_shadow(img, layer, -3, -3, blur, 14 | (0, 0, 0), 80.0, False) 15 | 16 | # Clean up 17 | pdb.gimp_selection_none(img) 18 | pdb.gimp_undo_push_group_end(img) 19 | 20 | 21 | register( 22 | "python_fu_blobify", 23 | "Create a 3-D effect", 24 | "Create a blobby 3-D effect using inverse drop-shadow", 25 | "Akkana Peck", 26 | "Akkana Peck", 27 | "2009", 28 | "BlobiPy...", 29 | "*", 30 | [ 31 | (PF_IMAGE, "image", "Input image", None), 32 | (PF_DRAWABLE, "drawable", "Input layer", None), 33 | (PF_SPINNER, "blur", "Blur amount", 34 | 7, (0, 50, 1)) 35 | ], 36 | [], 37 | python_blobify, 38 | menu="/Filters/Light and Shadow") 39 | 40 | 41 | main() 42 | -------------------------------------------------------------------------------- /gimp2/changefont.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | '''Change font size, face and color for every text layer in a GIMP image. 4 | Only affects images at the top level: does not descend into layer groups. 5 | ''' 6 | 7 | from gimpfu import * 8 | 9 | def python_change_font(img, fontsize, fontface, fontcolor): 10 | pdb.gimp_image_undo_group_start(img) 11 | 12 | for l in img.layers: 13 | if not pdb.gimp_item_is_text_layer(l): 14 | continue 15 | pdb.gimp_text_layer_set_font_size(l, fontsize, 0) 16 | pdb.gimp_text_layer_set_font(l, fontface) 17 | pdb.gimp_text_layer_set_color(l, fontcolor) 18 | 19 | pdb.gimp_image_undo_group_end(img) 20 | 21 | register( 22 | "python_fu_change_font", 23 | "Change every text layer in an image to a new font and size", 24 | "Change every text layer in an image to a new font and size", 25 | "Akkana Peck", 26 | "Akkana Peck", 27 | "2016,2019", 28 | "Change Font in all Text Layers...", 29 | "*", 30 | [ 31 | (PF_IMAGE, "image", "Input image", None), 32 | (PF_SPINNER, "fontsize", "New Font Size", 33 | 14, (1, 50, 1)), 34 | (PF_FONT, "fontface", "New Font", "Sans"), 35 | (PF_COLOR, "fontcolor", "Color", (1.0, 1.0, 0.0)), 36 | ], 37 | [], 38 | python_change_font, 39 | menu="/Filters/Generic") 40 | 41 | main() 42 | -------------------------------------------------------------------------------- /gimp2/cutout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Cut out a selected band an image, either vertically or horizontally, 4 | # resizing the canvas appropriately afterward. 5 | # The selection must extend all the way across the image either 6 | # vertically or horizontally, but not both. 7 | 8 | from gimpfu import * 9 | 10 | def python_fu_cutout(img, drawable): 11 | pdb.gimp_message_set_handler(MESSAGE_BOX) 12 | exists, x1, y1, x2, y2 = pdb.gimp_selection_bounds(img) 13 | orient = None 14 | 15 | # Figure out whether we're cutting vertically or horizontally. 16 | # It's ridiculously hard to make a selectin exactly the width or 17 | # height of the image, so figure that 95% is close enough. 18 | thresh = .05 19 | if x1 < thresh * img.width and x2 > img.width * (1. - thresh): 20 | orient = 'h' 21 | if y1 <= thresh * img.height and y2 > img.height * (1. - thresh): 22 | if orient: 23 | pdb.gimp_message("Error: selection can't encompass entire image") 24 | return 25 | orient = 'v' 26 | if not orient: 27 | pdb.gimp_message("Must have a selection spanning entire width or height") 28 | print "Selection is %d-%d x %d-%d" % (x1, x2, y1, y2) 29 | return 30 | 31 | # Now orient is either 'h' or 'v'. 32 | 33 | img.undo_group_start() 34 | 35 | if orient == 'h': 36 | pdb.gimp_image_select_rectangle(img, CHANNEL_OP_REPLACE, 37 | 0, y2, img.width, img.height) 38 | pdb.gimp_edit_cut(drawable) 39 | 40 | pdb.gimp_image_select_rectangle(img, CHANNEL_OP_REPLACE, 41 | 0, y1, img.width, img.height - y2) 42 | flayer = pdb.gimp_edit_paste(drawable, True) 43 | 44 | pdb.gimp_floating_sel_anchor(flayer) 45 | pdb.gimp_image_resize(img, img.width, y1 + img.height - y2, 0, 0) 46 | pdb.gimp_selection_none(img) 47 | 48 | elif orient == 'v': 49 | pdb.gimp_image_select_rectangle(img, CHANNEL_OP_REPLACE, 50 | x2, 0, img.width - x2, img.height) 51 | pdb.gimp_edit_cut(drawable) 52 | 53 | pdb.gimp_image_select_rectangle(img, CHANNEL_OP_REPLACE, 54 | x1, 0, img.width - x2, img.height) 55 | flayer = pdb.gimp_edit_paste(drawable, True) 56 | 57 | pdb.gimp_floating_sel_anchor(flayer) 58 | pdb.gimp_image_resize(img, x1 + img.width - x2, img.height, 0, 0) 59 | pdb.gimp_selection_none(img) 60 | 61 | img.undo_group_end() 62 | 63 | register( 64 | "python_fu_cutout", 65 | "Cut a selected band from an image, either vertically or horizontally", 66 | "Cut a selected band from an image, either vertically or horizontally", 67 | "Akkana Peck", 68 | "Akkana Peck", 69 | "2013", 70 | "Cut out selected band", 71 | "*", 72 | [ 73 | (PF_IMAGE, "image", "Input image", None), 74 | (PF_DRAWABLE, "drawable", "Input drawable", None), 75 | ], 76 | [], 77 | python_fu_cutout, 78 | menu = "/Filters/Distorts/" 79 | ) 80 | 81 | main() 82 | -------------------------------------------------------------------------------- /gimp2/export-all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Offer to export all currently open GIMP images. 4 | 5 | # WARNING: USE OF THIS PLUG-IN IN ITS CURRENT STATE IS DANGEROUS! 6 | # It is untested and unfinished. 7 | # It may overwrite your files without asking, 8 | # burn your meatloaf, or do other bad things. 9 | # 10 | # This plug-in is currently just a skeleton! It is intended as a 11 | # jumping-off point for further work, not as a usable plug-in yet. 12 | # Please contribute to it and send github pull requests! 13 | 14 | # Copyright 2013 by Akkana Peck, http://www.shallowsky.com/software/ 15 | # You may use and distribute this plug-in under the terms of the GPL v2 16 | # or, at your option, any later GPL version. 17 | 18 | from gimpfu import * 19 | import gtk 20 | import os 21 | 22 | # A way to show errors to the user: 23 | warning_label = None 24 | 25 | # Text entries containing the filenames: 26 | entries = [] 27 | # Save a list of images corresponding to each entry. 28 | # Don't depend on gimp.image_list remaining unchanged. 29 | images = [] 30 | 31 | def python_export_all() : 32 | dialog = gtk.Dialog("Export All", None, 0, 33 | (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 34 | gtk.STOCK_OK, gtk.RESPONSE_OK)) 35 | 36 | # set_default_response only marks the button visually -- 37 | # it doesn't actually change behavior. 38 | dialog.set_default_response(gtk.RESPONSE_OK) 39 | 40 | def toggle_active(widget, which): 41 | entries[which].set_sensitive(widget.get_active()) 42 | 43 | # Build the table of filenames: 44 | table = gtk.Table(2, len(gimp.image_list())) 45 | table.set_row_spacings(10) 46 | table.set_col_spacings(10) 47 | 48 | for i, img in enumerate(gimp.image_list()): 49 | entries.append(gtk.Entry()) 50 | images.append(img) 51 | entries[i].set_text(img.filename) 52 | 53 | # entries[i].set_width_chars(10) 54 | # entries[i].connect("activate", ...) 55 | # entries[i].connect("changed", ...) 56 | 57 | table.attach(entries[i], 0, 1, i, i+1) 58 | 59 | # Toggle button so we can control which images get saved 60 | toggle = gtk.ToggleButton("Save") 61 | toggle.set_active(True) 62 | toggle.connect("toggled", toggle_active, i) 63 | table.attach(toggle, 1, 2, i, i+1) 64 | 65 | dialog.vbox.pack_start(table, True, True, 0) 66 | dialog.show_all() 67 | 68 | response = dialog.run() 69 | 70 | if response != gtk.RESPONSE_OK : 71 | return 72 | 73 | for i, entry in enumerate(entries): 74 | if entry.get_sensitive(): 75 | # Save the image's active layer 76 | filename = entry.get_text() 77 | print "Saving", filename 78 | pdb.gimp_file_save(images[i], images[i].active_layer, 79 | filename, filename) 80 | pdb.gimp_image_clean_all(img) 81 | 82 | register( 83 | "python_fu_export_all", 84 | "Export all open images.", 85 | "Export all open images.", 86 | "Akkana Peck", 87 | "Akkana Peck", 88 | "2013", 89 | "Export all...", 90 | "*", 91 | [ 92 | ], 93 | [], 94 | python_export_all, 95 | menu = "/File/Save/" 96 | ) 97 | 98 | main() 99 | 100 | -------------------------------------------------------------------------------- /gimp2/export-scaled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Export a copy of the current image, scaled to a different size. 4 | # It's like "Save a copy" only it remembers to re-scale each time 5 | # back to whatever scale you used on the last export -- 6 | # like if you want to keep a thumbnail or a 7 | # smaller web version current as you're editing the master. 8 | 9 | # Copyright 2012 by Akkana Peck, http://www.shallowsky.com/software/ 10 | # You may use and distribute this plug-in under the terms of the GPL v2 11 | # or, at your option, any later GPL version. 12 | 13 | from gimpfu import * 14 | import gtk 15 | import os 16 | 17 | # Image original size 18 | orig_width = 0 19 | orig_height = 0 20 | 21 | # The three gtk.Entry widgets we'll monitor: 22 | percent_e = None 23 | width_e = None 24 | height_e = None 25 | 26 | # A way to show errors to the user: 27 | warning_label = None 28 | 29 | # A way to turn off notifications while we're calculating 30 | busy = False 31 | 32 | # Doesn't work to compare entry against percent_e, etc. 33 | # Must pass that info in a separate arg, darnit. 34 | def entry_changed(entry, which) : 35 | global percent_e, width_e, height_e, orig_width, orig_height, busy 36 | if busy : 37 | return 38 | 39 | # Can't use get_value() or get_value_as_int() because they 40 | # don't work before an update(), but update() will usually 41 | # overwrite what the user typed. So define a function: 42 | def get_num_value(spinbox) : 43 | s = spinbox.get_text() 44 | if not s : 45 | return 0 46 | 47 | try : 48 | p = int(s) 49 | return p 50 | except ValueError : 51 | try : 52 | p = float(s) 53 | return p 54 | except ValueError : 55 | return 0 56 | 57 | if which == 'p' : 58 | p = get_num_value(percent_e) 59 | if not p : return 60 | busy = True 61 | w = int(orig_width * p / 100.) 62 | width_e.set_text(str(w)) 63 | h = int(orig_height * p / 100.) 64 | height_e.set_text(str(h)) 65 | busy = False 66 | 67 | elif which == 'w' : 68 | w = get_num_value(width_e) 69 | if not w : return 70 | busy = True 71 | p = w * 100. / orig_width 72 | percent_e.set_text(str(p)) 73 | h = int(orig_height * p / 100.) 74 | height_e.set_text(str(h)) 75 | busy = False 76 | 77 | elif which == 'h' : 78 | h = get_num_value(height_e) 79 | if not h : return 80 | busy = True 81 | p = h * 100. / orig_height 82 | percent_e.set_text(str(p)) 83 | w = int(orig_width * p / 100.) 84 | width_e.set_text(str(w)) 85 | busy = False 86 | else : 87 | print "I'm confused -- not width, height or percentage" 88 | 89 | def python_export_scaled(img, drawable) : 90 | global percent_e, width_e, height_e, orig_width, orig_height, warning_label 91 | orig_filename = img.filename 92 | 93 | orig_width = img.width 94 | orig_height = img.height 95 | 96 | # Do we already have defaults? 97 | para = img.parasite_find('export-scaled') 98 | if para : 99 | [ init_filename, init_percent, init_width, init_height ] = \ 100 | para.data.split('\n') 101 | else : 102 | init_filename = orig_filename 103 | init_percent = 100 104 | init_width = img.width 105 | init_height = img.height 106 | 107 | chooser = gtk.FileChooserDialog(title=None, 108 | action=gtk.FILE_CHOOSER_ACTION_SAVE, 109 | buttons=(gtk.STOCK_CANCEL, 110 | gtk.RESPONSE_CANCEL, 111 | gtk.STOCK_SAVE, 112 | gtk.RESPONSE_OK)) 113 | 114 | if init_filename : 115 | chooser.set_current_name(os.path.basename(init_filename)) 116 | chooser.set_current_folder(os.path.dirname(init_filename)) 117 | 118 | vbox = gtk.VBox(spacing=8) 119 | 120 | hbox = gtk.HBox(spacing=8) 121 | 122 | l = gtk.Label("Export size:") 123 | l.set_alignment(1, 0) 124 | l.show() 125 | hbox.pack_start(l) 126 | 127 | l = gtk.Label("Percent") 128 | l.set_alignment(1, 0) 129 | hbox.pack_start(l) 130 | l.show() 131 | adj = gtk.Adjustment(float(init_percent), 1, 10000, 1, 10, 0) 132 | percent_e = gtk.SpinButton(adj) #, 0, 0) 133 | 134 | #percent_e = gtk.Entry() 135 | #percent_e.set_width_chars(5) 136 | #percent_e.set_text("100") # XXX Later, remember size from last time 137 | 138 | percent_e.connect("changed", entry_changed, 'p'); 139 | percent_e.show() 140 | hbox.pack_start(percent_e) 141 | l = gtk.Label("%") 142 | l.set_alignment(0, 1) 143 | hbox.pack_start(l) 144 | l.show() 145 | 146 | l = gtk.Label("Width") 147 | l.set_alignment(1, 0) 148 | hbox.pack_start(l) 149 | l.show() 150 | adj = gtk.Adjustment(float(init_width), 1, 10000, 1, 10, 0) 151 | width_e = gtk.SpinButton(adj) 152 | #width_e = gtk.Entry() 153 | #width_e.set_width_chars(7) 154 | #width_e.set_text(str(img.width)) 155 | # XXX Later, remember size from previous run 156 | width_e.connect("changed", entry_changed, 'w'); 157 | width_e.show() 158 | hbox.pack_start(width_e) 159 | 160 | l = gtk.Label("Height") 161 | l.set_alignment(1, 0) 162 | hbox.pack_start(l) 163 | l.show() 164 | adj = gtk.Adjustment(float(init_height), 1, 10000, 1, 10, 0) 165 | height_e = gtk.SpinButton(adj) 166 | #height_e = gtk.Entry() 167 | #height_e.set_width_chars(7) 168 | #height_e.set_text(str(img.height)) 169 | height_e.connect("changed", entry_changed, 'h'); 170 | hbox.pack_start(height_e) 171 | height_e.show() 172 | 173 | hbox.show() 174 | vbox.pack_start(hbox) 175 | 176 | warning_label = gtk.Label("") 177 | warning_label.show() 178 | vbox.pack_start(warning_label) 179 | 180 | chooser.set_extra_widget(vbox) 181 | 182 | # Oh, cool, we could have shortcuts to image folders, 183 | # and maybe remove the stupid fstab shortcuts GTK adds for us. 184 | #chooser.add_shortcut_folder(folder) 185 | #chooser.remove_shortcut_folder(folder) 186 | 187 | # Loop to catch errors/warnings: 188 | while True : 189 | response = chooser.run() 190 | if response != gtk.RESPONSE_OK: 191 | chooser.destroy() 192 | return 193 | 194 | percent = float(percent_e.get_text()) 195 | width = int(width_e.get_text()) 196 | height = int(height_e.get_text()) 197 | 198 | filename = chooser.get_filename() 199 | if filename == orig_filename : 200 | warning_label.set_text("Change the name or the directory -- don't overwrite original file!") 201 | continue 202 | 203 | # Whew, it's not the original filename, so now we can export. 204 | 205 | #print "export from", orig_filename, "to", filename, \ 206 | # "at", width, "x", height 207 | 208 | # Is there any point to pushing and popping the context? 209 | #gimp.context_pop() 210 | 211 | newimg = pdb.gimp_image_duplicate(img) 212 | 213 | # If it's XCF, we don't need to flatten or process it further, 214 | # just scale it: 215 | base, ext = os.path.splitext(filename) 216 | ext = ext.lower() 217 | if ext == '.gz' or ext == '.bz2' : 218 | base, ext = os.path.splitext(base) 219 | ext = ext.lower() 220 | if ext != '.xcf' : 221 | newimg.flatten() 222 | # XXX This could probably be smarter about flattening. Oh, well. 223 | pdb.gimp_image_merge_visible_layers(copyimg, CLIP_TO_IMAGE) 224 | 225 | newimg.scale(width, height) 226 | 227 | # Find any image type settings parasites (e.g. jpeg settings) 228 | # that got set during save, so we'll be able to use them 229 | # next time. 230 | def copy_settings_parasites(fromimg, toimg) : 231 | for pname in fromimg.parasite_list() : 232 | if pname[-9:] == '-settings' or pname[-13:] == '-save-options' : 233 | para = fromimg.parasite_find(pname) 234 | if para : 235 | toimg.attach_new_parasite(pname, para.flags, para.data) 236 | 237 | # Copy any settings parasites we may have saved from previous runs: 238 | copy_settings_parasites(img, newimg) 239 | 240 | # gimp-file-save insists on being passed a valid layer, 241 | # even if saving to a multilayer format such as XCF. Go figure. 242 | try : 243 | # I don't get it. What's the rule for whether gimp_file_save 244 | # will prompt for jpg parameters? Saving back to an existing 245 | # file doesn't seem to help. 246 | # run_mode=RUN_WITH_LAST_VALS is supposed to prevent prompting, 247 | # but actually seems to do nothing. Copying the -save-options 248 | # parasites is more effective. 249 | dpy = chooser.get_display() 250 | chooser.hide() 251 | dpy.sync() 252 | pdb.gimp_file_save(newimg, newimg.active_layer, filename, filename, 253 | run_mode=RUN_WITH_LAST_VALS) 254 | except RuntimeError, e : 255 | #warning_label.set_text("Didn't save to " + filename 256 | # + ":" + str(e)) 257 | markup = '' 258 | markup_end = '' 259 | warning_label.set_markup(markup + "Didn't save to " + filename 260 | + ":" + str(e) + markup_end) 261 | gimp.delete(newimg) 262 | chooser.show() 263 | continue 264 | 265 | chooser.destroy() 266 | 267 | #gimp.context_pop() 268 | 269 | copy_settings_parasites(newimg, img) 270 | 271 | gimp.delete(newimg) 272 | #gimp.Display(newimg) 273 | 274 | # Save parameters as a parasite, even if saving failed, 275 | # so we can default to them next time. 276 | # Save: percent, width, height 277 | # I have no idea what the flags are -- the doc doesn't say. 278 | para = img.attach_new_parasite('export-scaled', 0, 279 | '%s\n%d\n%d\n%d' % (filename, percent, 280 | width, height)) 281 | # img.parasite_attach(para) 282 | 283 | return 284 | 285 | register( 286 | "python_fu_export_scaled", 287 | "Export a copy of the current image, scaled to a different size.", 288 | "Export a copy of the current image, scaled to a different size.", 289 | "Akkana Peck", 290 | "Akkana Peck", 291 | "2012", 292 | "Export scaled...", 293 | "*", 294 | [ 295 | (PF_IMAGE, "image", "Input image", None), 296 | (PF_DRAWABLE, "drawable", "Input drawable", None), 297 | ], 298 | [], 299 | python_export_scaled, 300 | menu = "/File/Save/" 301 | ) 302 | 303 | main() 304 | 305 | -------------------------------------------------------------------------------- /gimp2/fibonacci.scm: -------------------------------------------------------------------------------- 1 | ;; fibonacci.scm: Make a Fibonacci grid and spiral, 2 | ;; optionally with an animated spiral image too. 3 | ;; Copyright (C) 2006 by Akkana Peck, akkana@shallowsky.com. 4 | ;; 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License. 7 | 8 | (define (script-fu-fibonacci-grid width box-color spiral-color 9 | font font-size text-color animatedp) 10 | (let* ((old-fg-color (car (gimp-context-get-foreground))) 11 | (grid-spacing (/ width 21)) 12 | (height (* grid-spacing 13)) 13 | ;; Make the new image 14 | (newimg (car (gimp-image-new width height RGB))) 15 | ;; and a background layer 16 | (bg-l (car (gimp-layer-new newimg width height 17 | RGBA-IMAGE "background" 18 | 100 NORMAL-MODE))) 19 | (grid-l (car (gimp-layer-new newimg width height 20 | RGBA-IMAGE "grid" 21 | 100 NORMAL-MODE))) 22 | (spiral-l (car (gimp-layer-new newimg width height 23 | RGBA-IMAGE "spiral" 24 | 100 NORMAL-MODE))) 25 | (text-l (car (gimp-layer-new newimg width height 26 | RGBA-IMAGE "labels" 27 | 100 NORMAL-MODE))) 28 | (floating-l nil) 29 | ;; If we're making an animated version, we need another image: 30 | (animated-im (if (= animatedp TRUE) 31 | (car (gimp-image-new width height RGB)) 32 | nil)) 33 | ;; Initial parameters for the golden spiral: 34 | (theta 0) 35 | (lastx 5.5) 36 | (lasty 3.5) 37 | (firstx (+ lastx .5 )) 38 | (firsty lasty) 39 | ) 40 | 41 | (gimp-image-undo-disable newimg) 42 | (if (= animatedp TRUE) (gimp-image-undo-disable animated-im)) 43 | (gimp-image-add-layer newimg bg-l -1) 44 | (gimp-image-add-layer newimg grid-l -1) 45 | (gimp-image-add-layer newimg spiral-l -1) 46 | (gimp-layer-set-opacity spiral-l 65) 47 | (gimp-image-add-layer newimg text-l -1) 48 | 49 | (gimp-drawable-fill bg-l BACKGROUND-FILL) 50 | (gimp-edit-clear grid-l) 51 | (gimp-edit-clear spiral-l) 52 | (gimp-edit-clear text-l) 53 | 54 | ; Horizontal lines 55 | (gimp-context-set-foreground box-color) 56 | (fibonacci-draw-line grid-l 0 0 21 0) 57 | (fibonacci-draw-line grid-l 5 3 8 3) 58 | (fibonacci-draw-line grid-l 5 4 6 4) 59 | (fibonacci-draw-line grid-l 0 5 8 5) 60 | (fibonacci-draw-line grid-l 0 13 21 13) 61 | ; Vertical lines 62 | (fibonacci-draw-line grid-l 0 0 0 13) 63 | (fibonacci-draw-line grid-l 5 0 5 5) 64 | (fibonacci-draw-line grid-l 6 3 6 5) 65 | (fibonacci-draw-line grid-l 8 0 8 13) 66 | (fibonacci-draw-line grid-l 21 0 21 13) 67 | ; Number the squares 68 | (gimp-context-set-foreground text-color) 69 | (fibonacci-label newimg text-l 2.5 2.5 "5" font font-size) 70 | (fibonacci-label newimg text-l 6.5 1.5 "3" font font-size) 71 | (fibonacci-label newimg text-l 5.5 3.5 "1" font font-size) 72 | (fibonacci-label newimg text-l 5.5 4.5 "1" font font-size) 73 | (fibonacci-label newimg text-l 7 4 "2" font font-size) 74 | (fibonacci-label newimg text-l 4 9 "8" font font-size) 75 | (fibonacci-label newimg text-l 14.5 6.5 "13" font font-size) 76 | 77 | ; Finally, draw a Golden Spiral. r = a*b^theta 78 | ; where b = phi^(1/90) ~= 1.35846 for radians (1.00536 for deg). 79 | ; and a is a scaling factor, grid-spacing. 80 | ; But since this is a Fibonacci spiral, not a real Golden spiral, 81 | ; we have to adjust it a little. 82 | (gimp-context-set-foreground spiral-color) 83 | (while (< theta 10) 84 | (let* ((b 1.378) 85 | (r (pow b theta)) 86 | (pi2 6.283184) 87 | (x (- firstx (/ (* r (cos theta)) 2))) 88 | (y (+ firsty (/ (* r (sin theta)) 2))) 89 | ) 90 | (fibonacci-draw-line spiral-l lastx lasty x y) 91 | ; This list->vector approach works in tiny-fu, 92 | ; but not in GIMP 2.2 or earlier: 93 | ;(gimp-paintbrush spiral-l 0 4 (list->vector 94 | ; (list lastx lasty x y)) 0 0) 95 | (set! theta (+ theta 0.1)) 96 | (set! lastx x) 97 | (set! lasty y) 98 | 99 | ;; Copy it to the animated image, if appropriate 100 | (if (= animatedp TRUE) 101 | (let ((anim-l (car (gimp-layer-new animated-im width height 102 | RGBA-IMAGE "100ms" 103 | 100 NORMAL-MODE))) 104 | ) 105 | (gimp-image-add-layer animated-im anim-l -1) 106 | (gimp-edit-copy-visible newimg) 107 | (set! floating-l (car (gimp-edit-paste anim-l TRUE))) 108 | (gimp-floating-sel-anchor floating-l) 109 | )) 110 | )) 111 | 112 | ;; Clean up 113 | (gimp-context-set-foreground old-fg-color) 114 | (gimp-image-undo-enable newimg) 115 | (if (= animatedp TRUE) (gimp-image-undo-enable animated-im)) 116 | (gimp-display-new newimg) 117 | (if (= animatedp TRUE) (gimp-display-new animated-im)) 118 | 119 | ;; Something about this script, maybe all the cons-arrays, 120 | ;; allocates way too much memory, and script-fu doesn't 121 | ;; clean up when it's done, which can make the rest of GIMP 122 | ;; nonfunctional. So force a garbage collect before returning: 123 | (gc) 124 | )) 125 | 126 | (define (fibonacci-draw-line drawable x1 y1 x2 y2) 127 | (let (;(points (cons-array 4 'double)) 128 | (spacing (/ (car (gimp-drawable-width drawable)) 21)) 129 | ) 130 | ;(aset points 0 (* x1 spacing)) 131 | ;(aset points 1 (* y1 spacing)) 132 | ;(aset points 2 (* x2 spacing)) 133 | ;(aset points 3 (* y2 spacing)) 134 | ;(gimp-pencil drawable 4 points) 135 | (gimp-paintbrush drawable 0 4 (list->vector (list (* x1 spacing) 136 | (* y1 spacing) 137 | (* x2 spacing) 138 | (* y2 spacing))) 0 0) 139 | )) 140 | 141 | (define (fibonacci-label img text-l x y text font font-size) 142 | (let* ((spacing (/ (car (gimp-drawable-width text-l)) 21)) 143 | ;; Text extents are needed for centering: 144 | (text-extents (gimp-text-get-extents-fontname text 145 | font-size PIXELS 146 | font)) 147 | ;; Create the text: 148 | (floating-l 149 | (car (gimp-text-fontname img text-l 150 | (- (* x spacing) (/ (car text-extents) 2)) 151 | (- (* y spacing) (/ (cadr text-extents) 2)) 152 | text 1 TRUE 153 | font-size PIXELS font))) 154 | ) 155 | (gimp-floating-sel-anchor floating-l) 156 | )) 157 | 158 | (script-fu-register "script-fu-fibonacci-grid" 159 | _"/File/Create/Misc/Fibonacci Grid..." 160 | "Make a Fibonacci grid and spirals" 161 | "Akkana Peck" 162 | "Akkana Peck" 163 | "November 2006" 164 | "" 165 | SF-ADJUSTMENT _"Width (pixels)" '(1024 500 3200 1 10 0 1) 166 | SF-COLOR _"Box color" '(0 0 0) 167 | SF-COLOR _"Spiral color" '(0 0 255) 168 | SF-FONT _"Font" "Arial Bold Italic" 169 | SF-ADJUSTMENT _"Font size (pixels)" '(30 2 500 1 10 0 1) 170 | SF-COLOR _"Text color" '(255 0 0) 171 | SF-TOGGLE _"Animated" FALSE 172 | ) 173 | 174 | -------------------------------------------------------------------------------- /gimp2/gimplabels/ChangeLog: -------------------------------------------------------------------------------- 1 | Change Log: 2 | 3 | 10/20/2007: 0.5 4 | Add Printer fudge factor to label page script, and make the 5 | install documentation a bit clearer. 6 | 7 | 12/4/2006: 0.4 8 | Changes needed to run in Tiny-Fu and GIMP 2.3. 9 | Fix a page size bug: label page was choosing A4 instead of Letter. 10 | Add a "Transparent?" option to Make Label Page. 11 | 12 | 5/23/2006: 0.3 13 | Use Copy Visible so that the label page can handle XCF labels with 14 | multiple layers. Reorganize menus so as not to use Script-Fu menus. 15 | 16 | 2/8/2005: 0.2 17 | Look for files in ~/.glabels for additional templates 18 | when generating labeltemplates.scm. 19 | 20 | Fix a bug involving iterating from the wrong end of the list. 21 | 22 | 2/2/2005: 0.1 23 | First release. 24 | 25 | -------------------------------------------------------------------------------- /gimp2/gimplabels/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for gimp label templates script-fu 2 | 3 | GIMPTOOL = gimptool-2.0 4 | 5 | labels.scm: labeltemplates.scm labels.scm-dist 6 | cat labeltemplates.scm labels.scm-dist >labels.scm 7 | 8 | install: labels.scm 9 | $(GIMPTOOL) --install-script labels.scm 10 | 11 | labeltemplates.scm: 12 | if ! test -s labeltemplates.scm; then cp labeltemplates.scm-dist labeltemplates.scm; fi 13 | 14 | templates: 15 | make-label-fu.py >labeltemplates.scm 16 | 17 | clean: 18 | rm labels.scm 19 | 20 | -------------------------------------------------------------------------------- /gimp2/gimplabels/README: -------------------------------------------------------------------------------- 1 | GimpLabels 2 | 3 | This is a little pair of GIMP scripts which can make it easier to 4 | design and print labels, business cards and other sized objects in 5 | GIMP, using label templates from the gLabels program. 6 | 7 | Also, us-letter paper size is the only supported paper so far. 8 | Modifying the code would be straightforward, though. 9 | 10 | BUILD/INSTALL OPTIONS 11 | 12 | First: the easy way to install if you just want to try out the 13 | script with the labels provided is to copy labels.scm into your 14 | gimp scripts directory. For example, on Linux with GIMP 2.4, 15 | that would be: 16 | cp labels.scm ~/.gimp-2.4/scripts/ 17 | 18 | If you want more control or want to add your own labels, read on: 19 | 20 | make will build labels.scm, using whatever label templates are 21 | currently in this directory. gimplabels is shipped with a set 22 | of labels, but if you have a recent version of glabels installed 23 | you may want to update the list by typing: 24 | 25 | make templates will update labeltemplates.scm, copying templates 26 | from your gLabels setup in /usr/share/glabels/templates. If your 27 | glabels is installed anywhere else, you will have to modify the 28 | make-label-fu.py program. make-label-fu.py also depends on having 29 | python and py-xml installed; if you don't have those, you're probably 30 | best off using the label templates shipped with gimplabels. 31 | 32 | make install will install the script to your personal gimp-2.0 or 33 | gimp-2.2 directory. It relies on having "gimptool" installed. 34 | On some distributions, this is packaged separately from gimp, 35 | in a package called gimp-devel or gimp-dev. If you can't install 36 | gimptool, you can simply cp labels.scm to ~/.gimp-2.x/scripts/ 37 | 38 | HOW TO USE GIMPLABELS 39 | 40 | Once installed, the scripts should show up in two places: 41 | /Xtns/Script-Fu/Labels/Rect Label... 42 | Make a new image with the right aspect ratio for the particular 43 | rectangular label. 44 | 45 | /Script-Fu/Labels/Rect Label Page... 46 | Make an image of a page of labels, populating it with the 47 | current label image. 48 | 49 | Note: the "Printer fudge factor" parameter is to compensate 50 | for the fact that gutenprint printer drivers never seem to use 51 | quite the whole page. The default is 0.968, which is about right 52 | for my Epson C86. I recommend that you print out some test pages 53 | (in draft mode on cheap paper) and figure out the right fudge 54 | factor for your printer, then edit the script so that will 55 | be the default (see the SF-ADJUSTMENT for "Printer fudge factor" 56 | near the end of the labels.scm). 57 | 58 | /Xtns/Script-Fu/Labels/CD Label... 59 | Make a CD label blank, with the inner part cut out. 60 | 61 | /Script-Fu/Labels/CD Mask 62 | Cut out a CD shape from the current image. 63 | 64 | AUTHORSHIP AND COPYRIGHT 65 | 66 | gimplabels is copyright (C) 2005,2006,2007 by Akkana Peck, 67 | and is licensed under the GPL: share and enjoy! 68 | As usual, it's provided with no warranty, etc. 69 | But if you have questions or enhancements, feel free to 70 | contact the author: Akkana Peck, akkana@shallowsky.com. 71 | 72 | -------------------------------------------------------------------------------- /gimp2/gimplabels/labels.scm: -------------------------------------------------------------------------------- 1 | ; labeltemplates.scm: Templates for labels.scm. 2 | ; Copyright (C) 2005,2009 by Akkana Peck, akkana@shallowsky.com. 3 | ; 4 | ; This program is free software; you can redistribute it and/or modify 5 | ; it under the terms of the GNU General Public License as published by 6 | ; the Free Software Foundation; either version 2 of the License, or 7 | ; (at your option) any later version. 8 | ; 9 | ; This program is distributed in the hope that it will be useful, 10 | ; but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | ; GNU General Public License for more details. 13 | ; 14 | ; You should have received a copy of the GNU General Public License 15 | ; along with this program; if not, write to the Free Software 16 | ; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 17 | 18 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 19 | ;; LABEL TEMPLATES 20 | ;; Eventually these should move to a separate, auto-generated file. 21 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 22 | 23 | ;; Rectangular labels: (name comment (w h) (nx ny x0 y0 dx dy)) 24 | (define label-templates-rectangular 25 | '( 26 | 27 | ;; File: /usr/share/glabels/templates/misc-us-templates.xml 28 | ( "OfficeMax OM99060" "Mailing Labels" ( 203.76 72.0 ) (3 10 0.0 36.0 203.76 72.0)) 29 | ( "Kingdom L" "Cassette Labels" ( 252.0 117.0 ) (2 6 33.12 27.0 261.0 126.144)) 30 | ( "Tough-Tags TTLW-2016" "Microtube labels" ( 92.16 36 ) (5 17 68 4.17 100.8 45.3)) 31 | ( "Netc 749303-70001 " "DLT Labels" ( 162 59.4 ) (3 10 36 45 189 72)) 32 | ( "Neato Slimline CD Case, spine" "Slimline CD Case (upside down)" ( 394.5 342.5 ) (1 2 96.5 41.9 0 363)) 33 | ( "Neato Slimline CD Case" "Slimline CD Case (rightside up)" ( 394.5 342.5 ) (1 2 130 41.9 0 363)) 34 | ( "Stomper PRO Spine" "PRO CD Labels 2-up (CD spine only)" ( 288 20 ) (2 1 18 385 288 0)) 35 | ( "Stomper PRO Zip" "PRO CD Labels 2-up (Face only)" ( 168 142 ) (1 2 407 68 0 142)) 36 | ( "Neato USCD2lbl Rectangles" "CD Template Rectangles" ( 77.04 234.36 ) (2 1 51.3 279.72 433.44 0)) 37 | ( "Southworth BC" "Business Cards" ( 252 144 ) (2 5 36 36 288 144)) 38 | 39 | ;; File: /usr/share/glabels/templates/avery-us-templates.xml 40 | ( "Avery LSK-3.5" "Divider Labels" ( 225.36 36.0 ) (1 20 36.0 36.0 315.36 36.0)) 41 | ( "Avery LSK-3" "Divider Labels" ( 225.36 36.0 ) (2 20 36.0 36.0 315.36 36.0)) 42 | ( "Avery LSK-5.5" "Divider Labels" ( 126.0 36.0 ) (2 20 36.0 21.6 147.6 36.0)) 43 | ( "Avery LSK-5" "Divider Labels" ( 126.0 36.0 ) (4 20 36.0 21.6 147.6 36.0)) 44 | ( "Avery LSK-8.5" "Divider Labels" ( 81.36 36.0 ) (2 20 36.0 36.0 153.36 36.0)) 45 | ( "Avery LSK-8" "Divider Labels" ( 81.36 36.0 ) (4 20 36.0 36.0 153.36 36.0)) 46 | ( "Avery 3274.1" "Square Labels" ( 180.0 180.0 ) (3 3 22.5 90.0 193.5 216.0)) 47 | ( "Avery 8165" "Full Sheet Labels" ( 612.0 792.0 ) (1 1 0 0 0 0)) 48 | ( "Avery 6570" "ID Labels" ( 126.0 90.0 ) (4 8 36.0 36.0 139.536 90.0)) 49 | ( "Avery 5997-Spine" "Video Tape Spine Labels" ( 414 48 ) (1 15 99 36 0 48)) 50 | ( "Avery 5997-Face" "Video Tape Face Labels" ( 220 133 ) (2 5 80 60.5 236 133)) 51 | ( "Avery 5931-Spine" "CD/DVD Labels (Spine Labels)" ( 15.75 337.5 ) (2 2 36.0 52.875 33.75 348.75)) 52 | ( "Avery 5395" "Name Badge Labels" ( 243.0 167.999999976 ) (2 4 49.5 41.999999976 270.0 180.0)) 53 | ( "Avery 8373" "Business Cards" ( 252.0 144.0 ) (2 4 36.0 54.0 288.0 180.0)) 54 | ( "Avery 5389" "Post cards" ( 432.0 288.0 ) (1 2 90.0 90.0 432.0 324.0)) 55 | ( "Avery 5388" "Index Cards" ( 360.0 216.0 ) (1 3 126.0 72.0 360.0 216.0)) 56 | ( "Avery 5371" "Business Cards" ( 252.0 144.0 ) (2 5 54.0 36.0 252.0 144.0)) 57 | ( "Avery 5366" "Filing Labels" ( 247.5 48.000000024 ) (2 15 38.25 36.0 288.0 48.000000024)) 58 | ( "Avery 6490" "Diskette Labels" ( 193.5 144.0 ) (3 5 9.0 36.0 200.25 144.0)) 59 | ( "Avery 5663" "Address Labels" ( 306.0 144.0 ) (2 5 0.0 36.0 306.0 144.0)) 60 | ( "Avery 5197" "Address Labels" ( 288.0 108.0 ) (2 6 13.5 72.0 297.0 108.0)) 61 | ( "Avery 5196" "Diskette Labels" ( 198.0 198.0 ) (3 3 9.0 36.0 198.0 216.0)) 62 | ( "Avery 5167" "Return Address Labels" ( 126.0 36.0 ) (4 20 20.25 36.0 148.5 36.0)) 63 | ( "Avery 5168" "Shipping Labels" ( 252.0 360.0 ) (2 2 36.0 36.0 288.0 360.0)) 64 | ( "Avery 5164" "Shipping Labels" ( 288.0 239.999999976 ) (2 3 11.25 36.0 301.5 239.999999976)) 65 | ( "Avery 5163" "Shipping Labels" ( 288.0 144.0 ) (2 5 11.7 36.0 301.5 144.0)) 66 | ( "Avery 5159" "Address Labels" ( 288.0 108.0 ) (2 7 11.25 18.0 301.5 108.0)) 67 | ( "Avery 6879" "Address Labels" ( 270.0 90.0 ) (2 6 27.0 81.0 288.0 108.0)) 68 | ( "Avery 5305" "Tent Cards" ( 612.0 180.0 ) (1 2 0.0 216.0 0.0 360.0)) 69 | ( "Avery 5162" "Address Labels" ( 288.0 95.999999976 ) (2 7 11.25 59.999999976 301.5 95.999999976)) 70 | ( "Avery 5161" "Address Labels" ( 288.0 72.0 ) (2 10 11.25 36.0 301.5 72.0)) 71 | ( "Avery 5160" "Address Labels" ( 189.0 72.0 ) (3 10 13.5 36.0 198.0 72.0)) 72 | ;; No personal glabels templates: [Errno 2] No such file or directory: '/home/akkana/.glabels' 73 | ) ) 74 | 75 | ;; CD Labels: (name comment (radius hole) (nx ny x0 y0 dx dy)) 76 | (define label-templates-cd 77 | '( 78 | ("Avery 5931" "CD Labels, 2 per sheet" (166.5 58.5) (1 2 139.5 49.5 0 360)) 79 | ) ) 80 | 81 | ;; Paper sizes we recognize: 82 | ;; Note, this is currently somewhat bogus because the label templates are 83 | ;; specific to a particular paper size. 84 | (define page-sizes 85 | '( 86 | ("a4" 595.276 841.89) 87 | ("us-letter" 612 792) 88 | )) 89 | 90 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 91 | ;; END LABEL TEMPLATES 92 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 93 | 94 | 95 | ; labels.scm: Make labels according to templates. 96 | ; Copyright (C) 2005,2006,2007 by Akkana Peck, akkana@shallowsky.com. 97 | ; Requires templates in labeltemplates.scm 98 | ; 99 | ; This program is free software; you can redistribute it and/or modify 100 | ; it under the terms of the GNU General Public License as published by 101 | ; the Free Software Foundation; either version 2 of the License, or 102 | ; (at your option) any later version. 103 | ; 104 | ; This program is distributed in the hope that it will be useful, 105 | ; but WITHOUT ANY WARRANTY; without even the implied warranty of 106 | ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 107 | ; GNU General Public License for more details. 108 | ; 109 | ; You should have received a copy of the GNU General Public License 110 | ; along with this program; if not, write to the Free Software 111 | ; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 112 | 113 | (define (find-label-template templatenum templatelist) 114 | ; (nth templatenum templatelist) ) 115 | (nth (- (length templatelist) templatenum 1) templatelist)) 116 | 117 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 118 | ;; Rectangular label routines 119 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 120 | ;; 121 | ;; Make a rectangular shape, in a solid color. 122 | ;; 123 | (define (script-fu-make-rect width height color name) 124 | (let* ((old-fg-color (car (gimp-context-get-foreground))) 125 | (img (car (gimp-image-new width height RGB))) 126 | (labellayer (car (gimp-layer-new img width height 127 | RGBA-IMAGE name 100 NORMAL-MODE)))) 128 | (gimp-image-undo-disable img) 129 | (gimp-image-add-layer img labellayer -1) 130 | (gimp-selection-all img) 131 | (gimp-edit-clear labellayer) 132 | 133 | (gimp-context-set-foreground color) 134 | (gimp-edit-bucket-fill labellayer 135 | FG-BUCKET-FILL NORMAL-MODE 100 0 FALSE 0 0) 136 | 137 | ;; Clean up 138 | (gimp-image-set-filename img name) ;; XXX should remove spaces 139 | (gimp-selection-none img) 140 | (gimp-image-set-active-layer img labellayer) 141 | (gimp-context-set-foreground old-fg-color) 142 | (gimp-image-undo-enable img) 143 | (gimp-display-new img))) 144 | 145 | ;; 146 | ;; Make a rectangle with the right aspect ratio for the specified template. 147 | ;; 148 | (define (script-fu-rect-label templatenum width color) 149 | (let* ((templ (find-label-template templatenum label-templates-rectangular))) 150 | (if (not (null? templ)) 151 | (let* ((w (car (car (cdr (cdr templ))))) 152 | (h (car (cdr (car (cdr (cdr templ)))))) 153 | (name (car templ)) 154 | ) 155 | (script-fu-make-rect width (/ (* width h) w) color name)) 156 | (gimp-message "Couldn't find that label template!") 157 | ))) 158 | 159 | ;; 160 | ;; Make a rectangle with the right aspect ratio for the specified template, 161 | ;; populating it with the current label. 162 | ;; 163 | (define (script-fu-rect-label-page img drawable 164 | templatenum from to trans fudge) 165 | (let* ( 166 | (templ (find-label-template templatenum label-templates-rectangular)) 167 | (pgsize (find-label-template 0 page-sizes)) 168 | ) 169 | (if (not (null? templ)) 170 | (let* ( 171 | (temp-layer nil) 172 | (dims (car (cdr (cdr templ)))) 173 | (imgwidth (car (gimp-image-width img))) 174 | (w (car dims)) 175 | (h (car (cdr dims))) 176 | (scale (/ imgwidth w)) 177 | (layouts (car (cdr (cdr (cdr templ))))) 178 | (nx (car layouts)) 179 | (ny (nth 1 layouts)) 180 | (x0 (* scale (nth 2 layouts))) 181 | (y0 (* scale (nth 3 layouts))) 182 | (dx (* scale (nth 4 layouts))) 183 | (dy (* scale (nth 5 layouts))) 184 | (pagew (* scale (nth 1 pgsize))) 185 | (pageh (* scale (nth 2 pgsize))) 186 | (name (string-append (car templ) " page")) 187 | 188 | ;; Make the new image 189 | (newimg (car (gimp-image-new pagew pageh RGB))) 190 | (baselayer (car (gimp-layer-new newimg pagew pageh 191 | (if (= trans TRUE) RGBA-IMAGE 192 | RGB-IMAGE) 193 | "background" 194 | 100 NORMAL-MODE))) 195 | ) 196 | 197 | ; (gimp-message (string-append "Making label page of width " 198 | ; (number->string w 10))) 199 | ; (gimp-message 200 | ; (string-append "w " (number->string w 10) 201 | ; ", h " (number->string h 10) 202 | ; ", nx " (number->string nx 10) 203 | ; ", ny " (number->string ny 10) 204 | ; ", x0 " (number->string x0 10) 205 | ; ", y0 " (number->string y0 10) 206 | ; ", dx " (number->string dx 10) 207 | ; ", dy " (number->string dy 10))) 208 | 209 | ; Set upper bound 210 | (if (= to 99) 211 | (set! to (* nx ny)) 212 | (if (< to from) (set! to from))) 213 | 214 | (gimp-image-undo-disable newimg) 215 | (gimp-image-add-layer newimg baselayer -1) 216 | (gimp-edit-clear baselayer) 217 | 218 | ;; Copy the label 219 | (gimp-edit-copy-visible img) 220 | 221 | ;; Loop, pasting copies of the label into the new image. 222 | (let* ( (i 0) (j 0) (x x0) (y y0) 223 | ;; Okay, this is totally cheatsie, looping over 224 | ;; everything on the page and not just the ones 225 | ;; between from and to. Sheesh! 226 | (num 1) 227 | ) 228 | (while (< j ny) 229 | (set! i 0) 230 | (set! x x0) 231 | (while (< i nx) 232 | (if (and (>= num from) (<= num to)) 233 | (let* ((floating-sel (car (gimp-edit-paste baselayer FALSE)))) 234 | (gimp-layer-set-offsets floating-sel x y) 235 | ; Either anchor or make a new layer 236 | (gimp-floating-sel-anchor floating-sel) 237 | ;(gimp-floating-sel-to-layer floating-sel) 238 | )) 239 | (set! num (+ num 1)) 240 | (set! i (+ i 1)) 241 | (set! x (+ x dx)) 242 | ) 243 | (set! j (+ j 1)) 244 | (set! y (+ y dy)) 245 | ;(set! y (+ y0 (* j dy))) 246 | )) 247 | 248 | ;; Crop the resulting image according to the printer fudge factor. 249 | ;; Yes, I know it would be better to just apply it to begin with, 250 | ;; but it complicates the code. 251 | (let ((fudgew (* pagew fudge)) 252 | (fudgeh (* pageh fudge))) 253 | (gimp-image-crop newimg fudgew fudgeh 254 | (/ (- pagew fudgew) 2) (/ (- pageh fudgeh) 2))) 255 | 256 | (gimp-image-set-filename newimg name) ;; XXX should remove spaces 257 | 258 | ;; Clean up 259 | (gimp-selection-none img) 260 | (gimp-image-undo-enable newimg) 261 | (gimp-display-new newimg) 262 | ) 263 | ) 264 | )) 265 | 266 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 267 | ;; CD label routines 268 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 269 | 270 | ;; Utility to calculate the inner radius of a CD template 271 | (define (cd-inner-diam diameter mini) 272 | (if (= mini TRUE) (/ diameter 2.2) (/ diameter 3.1))) 273 | 274 | ;; Select the CD shape. Then you can cut, or whatever. 275 | (define (CD-select img diameter mini) 276 | (gimp-ellipse-select img 0 0 diameter diameter CHANNEL-OP-REPLACE TRUE FALSE 0) 277 | (let* ( 278 | (inner (cd-inner-diam diameter mini)) 279 | (offset (/ (- diameter inner) 2)) 280 | ) 281 | (gimp-ellipse-select img offset offset inner inner CHANNEL-OP-SUBTRACT TRUE FALSE 0) 282 | )) 283 | 284 | ;; Make a CD shape, in a solid color. 285 | (define (script-fu-CD-label diameter color mini) 286 | (let* ((old-fg-color (car (gimp-context-get-foreground))) 287 | (img (car (gimp-image-new diameter diameter RGB))) 288 | (cdlayer (car (gimp-layer-new img diameter diameter 289 | RGBA-IMAGE "CD" 100 NORMAL-MODE)))) 290 | (gimp-image-undo-disable img) 291 | (gimp-image-add-layer img cdlayer -1) 292 | (gimp-selection-all img) 293 | (gimp-edit-clear cdlayer) 294 | 295 | (gimp-context-set-foreground color) 296 | (CD-select img diameter mini) 297 | (gimp-edit-bucket-fill cdlayer FG-BUCKET-FILL NORMAL-MODE 100 0 FALSE 0 0) 298 | 299 | ;; Clean up 300 | (gimp-selection-none img) 301 | (gimp-image-set-active-layer img cdlayer) 302 | (gimp-context-set-foreground old-fg-color) 303 | (gimp-image-undo-enable img) 304 | (gimp-display-new img))) 305 | 306 | ;; Cut out a CD shape from the current image. 307 | (define (script-fu-CD-mask img drawable mini) 308 | (gimp-image-undo-group-start img) 309 | (CD-select img 310 | (min (car (gimp-image-width img)) (car (gimp-image-height img))) 311 | mini) 312 | (gimp-selection-invert img) 313 | (gimp-edit-clear drawable) 314 | (gimp-image-undo-group-end img) 315 | (gimp-displays-flush) 316 | ) 317 | 318 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 319 | ;; Registering the script-fu routines. 320 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 321 | 322 | ;; Loop over all our templates, to register them 323 | (let* ((labelnames nil) 324 | (templates label-templates-rectangular) 325 | (pnames nil) 326 | (psizes page-sizes) 327 | ) 328 | (while (pair? templates) 329 | (let* ((curtempl (car templates)) 330 | (curname (car curtempl)) 331 | (curdesc (cadr curtempl))) 332 | (set! curname (string-append (car (car templates)) 333 | " " 334 | (car (cdr (car templates))))) 335 | (set! labelnames (cons curname labelnames)) 336 | (set! templates (cdr templates)) 337 | )) 338 | (while (pair? psizes) 339 | (set! pnames (cons (car (car psizes)) pnames)) 340 | (set! psizes (cdr psizes)) 341 | ) 342 | (script-fu-register "script-fu-rect-label" 343 | _"/File/Create/Misc/Rect label..." 344 | "Make a single rectangular template" 345 | "Akkana Peck" 346 | "Akkana Peck" 347 | "October 2007" 348 | "" 349 | SF-OPTION _"Template Name" labelnames 350 | SF-ADJUSTMENT _"Width" '(800 1 2000 10 50 0 1) 351 | SF-COLOR _"Color" '(255 255 255) 352 | ) 353 | 354 | (script-fu-register "script-fu-rect-label-page" 355 | _"/Filters/Combine/Make label page..." 356 | "Make a page full of rectangular templates from the current image" 357 | "Akkana Peck" 358 | "Akkana Peck" 359 | "October 2007" 360 | "RGB* GRAY* INDEXED*" 361 | SF-IMAGE "Image" 0 362 | SF-DRAWABLE "Drawable" 0 363 | SF-OPTION _"Template Name" labelnames 364 | ; SF-OPTION _"Paper Size" pnames 365 | SF-ADJUSTMENT _"From" '( 1 1 99 1 10 0 1) 366 | SF-ADJUSTMENT "To" '(99 1 99 1 10 0 1) 367 | SF-TOGGLE "Transparent background?" FALSE 368 | SF-ADJUSTMENT "Printer fudge factor" 369 | '(0.968 0.01 2 0.01 0.1 3 SF-SPINNER) 370 | ) 371 | ) 372 | 373 | (script-fu-register "script-fu-CD-label" 374 | _"/File/Create/Misc/CD label..." 375 | "CD label shape" 376 | "Akkana Peck" 377 | "Akkana Peck" 378 | "December 2002" 379 | "" 380 | SF-ADJUSTMENT _"Diameter" '(1024 1 2000 10 50 0 1) 381 | SF-COLOR _"Color" '(170 240 240) 382 | SF-TOGGLE _"Mini CD" FALSE) 383 | 384 | (script-fu-register "script-fu-CD-mask" 385 | _"/Filters/Render/CD mask..." 386 | "Select a CD label shape out of the current layer" 387 | "Akkana Peck" 388 | "Akkana Peck" 389 | "December 2002" 390 | "RGB* GRAY* INDEXED*" 391 | SF-IMAGE "Image" 0 392 | SF-DRAWABLE "Drawable" 0 393 | SF-TOGGLE _"Mini CD" FALSE) 394 | 395 | -------------------------------------------------------------------------------- /gimp2/gimplabels/labels.scm-dist: -------------------------------------------------------------------------------- 1 | ; labels.scm: Make labels according to templates. 2 | ; Copyright (C) 2005,2006,2007 by Akkana Peck, akkana@shallowsky.com. 3 | ; Requires templates in labeltemplates.scm 4 | ; 5 | ; This program is free software; you can redistribute it and/or modify 6 | ; it under the terms of the GNU General Public License as published by 7 | ; the Free Software Foundation; either version 2 of the License, or 8 | ; (at your option) any later version. 9 | ; 10 | ; This program is distributed in the hope that it will be useful, 11 | ; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ; GNU General Public License for more details. 14 | ; 15 | ; You should have received a copy of the GNU General Public License 16 | ; along with this program; if not, write to the Free Software 17 | ; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 18 | 19 | (define (find-label-template templatenum templatelist) 20 | ; (nth templatenum templatelist) ) 21 | (nth (- (length templatelist) templatenum 1) templatelist)) 22 | 23 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 24 | ;; Rectangular label routines 25 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 26 | ;; 27 | ;; Make a rectangular shape, in a solid color. 28 | ;; 29 | (define (script-fu-make-rect width height color name) 30 | (let* ((old-fg-color (car (gimp-context-get-foreground))) 31 | (img (car (gimp-image-new width height RGB))) 32 | (labellayer (car (gimp-layer-new img width height 33 | RGBA-IMAGE name 100 NORMAL-MODE)))) 34 | (gimp-image-undo-disable img) 35 | (gimp-image-add-layer img labellayer -1) 36 | (gimp-selection-all img) 37 | (gimp-edit-clear labellayer) 38 | 39 | (gimp-context-set-foreground color) 40 | (gimp-edit-bucket-fill labellayer 41 | FG-BUCKET-FILL NORMAL-MODE 100 0 FALSE 0 0) 42 | 43 | ;; Clean up 44 | (gimp-image-set-filename img name) ;; XXX should remove spaces 45 | (gimp-selection-none img) 46 | (gimp-image-set-active-layer img labellayer) 47 | (gimp-context-set-foreground old-fg-color) 48 | (gimp-image-undo-enable img) 49 | (gimp-display-new img))) 50 | 51 | ;; 52 | ;; Make a rectangle with the right aspect ratio for the specified template. 53 | ;; 54 | (define (script-fu-rect-label templatenum width color) 55 | (let* ((templ (find-label-template templatenum label-templates-rectangular))) 56 | (if (not (null? templ)) 57 | (let* ((w (car (car (cdr (cdr templ))))) 58 | (h (car (cdr (car (cdr (cdr templ)))))) 59 | (name (car templ)) 60 | ) 61 | (script-fu-make-rect width (/ (* width h) w) color name)) 62 | (gimp-message "Couldn't find that label template!") 63 | ))) 64 | 65 | ;; 66 | ;; Make a rectangle with the right aspect ratio for the specified template, 67 | ;; populating it with the current label. 68 | ;; 69 | (define (script-fu-rect-label-page img drawable 70 | templatenum from to trans fudge) 71 | (let* ( 72 | (templ (find-label-template templatenum label-templates-rectangular)) 73 | (pgsize (find-label-template 0 page-sizes)) 74 | ) 75 | (if (not (null? templ)) 76 | (let* ( 77 | (temp-layer nil) 78 | (dims (car (cdr (cdr templ)))) 79 | (imgwidth (car (gimp-image-width img))) 80 | (w (car dims)) 81 | (h (car (cdr dims))) 82 | (scale (/ imgwidth w)) 83 | (layouts (car (cdr (cdr (cdr templ))))) 84 | (nx (car layouts)) 85 | (ny (nth 1 layouts)) 86 | (x0 (* scale (nth 2 layouts))) 87 | (y0 (* scale (nth 3 layouts))) 88 | (dx (* scale (nth 4 layouts))) 89 | (dy (* scale (nth 5 layouts))) 90 | (pagew (* scale (nth 1 pgsize))) 91 | (pageh (* scale (nth 2 pgsize))) 92 | (name (string-append (car templ) " page")) 93 | 94 | ;; Make the new image 95 | (newimg (car (gimp-image-new pagew pageh RGB))) 96 | (baselayer (car (gimp-layer-new newimg pagew pageh 97 | (if (= trans TRUE) RGBA-IMAGE 98 | RGB-IMAGE) 99 | "background" 100 | 100 NORMAL-MODE))) 101 | ) 102 | 103 | ; (gimp-message (string-append "Making label page of width " 104 | ; (number->string w 10))) 105 | ; (gimp-message 106 | ; (string-append "w " (number->string w 10) 107 | ; ", h " (number->string h 10) 108 | ; ", nx " (number->string nx 10) 109 | ; ", ny " (number->string ny 10) 110 | ; ", x0 " (number->string x0 10) 111 | ; ", y0 " (number->string y0 10) 112 | ; ", dx " (number->string dx 10) 113 | ; ", dy " (number->string dy 10))) 114 | 115 | ; Set upper bound 116 | (if (= to 99) 117 | (set! to (* nx ny)) 118 | (if (< to from) (set! to from))) 119 | 120 | (gimp-image-undo-disable newimg) 121 | (gimp-image-add-layer newimg baselayer -1) 122 | (gimp-edit-clear baselayer) 123 | 124 | ;; Copy the label 125 | (gimp-edit-copy-visible img) 126 | 127 | ;; Loop, pasting copies of the label into the new image. 128 | (let* ( (i 0) (j 0) (x x0) (y y0) 129 | ;; Okay, this is totally cheatsie, looping over 130 | ;; everything on the page and not just the ones 131 | ;; between from and to. Sheesh! 132 | (num 1) 133 | ) 134 | (while (< j ny) 135 | (set! i 0) 136 | (set! x x0) 137 | (while (< i nx) 138 | (if (and (>= num from) (<= num to)) 139 | (let* ((floating-sel (car (gimp-edit-paste baselayer FALSE)))) 140 | (gimp-layer-set-offsets floating-sel x y) 141 | ; Either anchor or make a new layer 142 | (gimp-floating-sel-anchor floating-sel) 143 | ;(gimp-floating-sel-to-layer floating-sel) 144 | )) 145 | (set! num (+ num 1)) 146 | (set! i (+ i 1)) 147 | (set! x (+ x dx)) 148 | ) 149 | (set! j (+ j 1)) 150 | (set! y (+ y dy)) 151 | ;(set! y (+ y0 (* j dy))) 152 | )) 153 | 154 | ;; Crop the resulting image according to the printer fudge factor. 155 | ;; Yes, I know it would be better to just apply it to begin with, 156 | ;; but it complicates the code. 157 | (let ((fudgew (* pagew fudge)) 158 | (fudgeh (* pageh fudge))) 159 | (gimp-image-crop newimg fudgew fudgeh 160 | (/ (- pagew fudgew) 2) (/ (- pageh fudgeh) 2))) 161 | 162 | (gimp-image-set-filename newimg name) ;; XXX should remove spaces 163 | 164 | ;; Clean up 165 | (gimp-selection-none img) 166 | (gimp-image-undo-enable newimg) 167 | (gimp-display-new newimg) 168 | ) 169 | ) 170 | )) 171 | 172 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 173 | ;; CD label routines 174 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 175 | 176 | ;; Utility to calculate the inner radius of a CD template 177 | (define (cd-inner-diam diameter mini) 178 | (if (= mini TRUE) (/ diameter 2.2) (/ diameter 3.1))) 179 | 180 | ;; Select the CD shape. Then you can cut, or whatever. 181 | (define (CD-select img diameter mini) 182 | (gimp-ellipse-select img 0 0 diameter diameter CHANNEL-OP-REPLACE TRUE FALSE 0) 183 | (let* ( 184 | (inner (cd-inner-diam diameter mini)) 185 | (offset (/ (- diameter inner) 2)) 186 | ) 187 | (gimp-ellipse-select img offset offset inner inner CHANNEL-OP-SUBTRACT TRUE FALSE 0) 188 | )) 189 | 190 | ;; Make a CD shape, in a solid color. 191 | (define (script-fu-CD-label diameter color mini) 192 | (let* ((old-fg-color (car (gimp-context-get-foreground))) 193 | (img (car (gimp-image-new diameter diameter RGB))) 194 | (cdlayer (car (gimp-layer-new img diameter diameter 195 | RGBA-IMAGE "CD" 100 NORMAL-MODE)))) 196 | (gimp-image-undo-disable img) 197 | (gimp-image-add-layer img cdlayer -1) 198 | (gimp-selection-all img) 199 | (gimp-edit-clear cdlayer) 200 | 201 | (gimp-context-set-foreground color) 202 | (CD-select img diameter mini) 203 | (gimp-edit-bucket-fill cdlayer FG-BUCKET-FILL NORMAL-MODE 100 0 FALSE 0 0) 204 | 205 | ;; Clean up 206 | (gimp-selection-none img) 207 | (gimp-image-set-active-layer img cdlayer) 208 | (gimp-context-set-foreground old-fg-color) 209 | (gimp-image-undo-enable img) 210 | (gimp-display-new img))) 211 | 212 | ;; Cut out a CD shape from the current image. 213 | (define (script-fu-CD-mask img drawable mini) 214 | (gimp-image-undo-group-start img) 215 | (CD-select img 216 | (min (car (gimp-image-width img)) (car (gimp-image-height img))) 217 | mini) 218 | (gimp-selection-invert img) 219 | (gimp-edit-clear drawable) 220 | (gimp-image-undo-group-end img) 221 | (gimp-displays-flush) 222 | ) 223 | 224 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 225 | ;; Registering the script-fu routines. 226 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 227 | 228 | ;; Loop over all our templates, to register them 229 | (let* ((labelnames nil) 230 | (templates label-templates-rectangular) 231 | (pnames nil) 232 | (psizes page-sizes) 233 | ) 234 | (while (pair? templates) 235 | (let* ((curtempl (car templates)) 236 | (curname (car curtempl)) 237 | (curdesc (cadr curtempl))) 238 | (set! curname (string-append (car (car templates)) 239 | " " 240 | (car (cdr (car templates))))) 241 | (set! labelnames (cons curname labelnames)) 242 | (set! templates (cdr templates)) 243 | )) 244 | (while (pair? psizes) 245 | (set! pnames (cons (car (car psizes)) pnames)) 246 | (set! psizes (cdr psizes)) 247 | ) 248 | (script-fu-register "script-fu-rect-label" 249 | _"/Xtns/Misc/Rect label..." 250 | "Make a single rectangular template" 251 | "Akkana Peck" 252 | "Akkana Peck" 253 | "October 2007" 254 | "" 255 | SF-OPTION _"Template Name" labelnames 256 | SF-ADJUSTMENT _"Width" '(800 1 2000 10 50 0 1) 257 | SF-COLOR _"Color" '(255 255 255) 258 | ) 259 | 260 | (script-fu-register "script-fu-rect-label-page" 261 | _"/Filters/Combine/Make label page..." 262 | "Make a page full of rectangular templates from the current image" 263 | "Akkana Peck" 264 | "Akkana Peck" 265 | "October 2007" 266 | "RGB* GRAY* INDEXED*" 267 | SF-IMAGE "Image" 0 268 | SF-DRAWABLE "Drawable" 0 269 | SF-OPTION _"Template Name" labelnames 270 | ; SF-OPTION _"Paper Size" pnames 271 | SF-ADJUSTMENT _"From" '( 1 1 99 1 10 0 1) 272 | SF-ADJUSTMENT "To" '(99 1 99 1 10 0 1) 273 | SF-TOGGLE "Transparent background?" FALSE 274 | SF-ADJUSTMENT "Printer fudge factor" 275 | '(0.968 0.01 2 0.01 0.1 3 SF-SPINNER) 276 | ) 277 | ) 278 | 279 | (script-fu-register "script-fu-CD-label" 280 | _"/Xtns/Misc/CD label..." 281 | "CD label shape" 282 | "Akkana Peck" 283 | "Akkana Peck" 284 | "December 2002" 285 | "" 286 | SF-ADJUSTMENT _"Diameter" '(1024 1 2000 10 50 0 1) 287 | SF-COLOR _"Color" '(170 240 240) 288 | SF-TOGGLE _"Mini CD" FALSE) 289 | 290 | (script-fu-register "script-fu-CD-mask" 291 | _"/Filters/Render/CD mask..." 292 | "Select a CD label shape out of the current layer" 293 | "Akkana Peck" 294 | "Akkana Peck" 295 | "December 2002" 296 | "RGB* GRAY* INDEXED*" 297 | SF-IMAGE "Image" 0 298 | SF-DRAWABLE "Drawable" 0 299 | SF-TOGGLE _"Mini CD" FALSE) 300 | 301 | -------------------------------------------------------------------------------- /gimp2/gimplabels/labeltemplates.scm: -------------------------------------------------------------------------------- 1 | ; labeltemplates.scm: Templates for labels.scm. 2 | ; Copyright (C) 2005,2009 by Akkana Peck, akkana@shallowsky.com. 3 | ; 4 | ; This program is free software; you can redistribute it and/or modify 5 | ; it under the terms of the GNU General Public License as published by 6 | ; the Free Software Foundation; either version 2 of the License, or 7 | ; (at your option) any later version. 8 | ; 9 | ; This program is distributed in the hope that it will be useful, 10 | ; but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | ; GNU General Public License for more details. 13 | ; 14 | ; You should have received a copy of the GNU General Public License 15 | ; along with this program; if not, write to the Free Software 16 | ; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 17 | 18 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 19 | ;; LABEL TEMPLATES 20 | ;; Eventually these should move to a separate, auto-generated file. 21 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 22 | 23 | ;; Rectangular labels: (name comment (w h) (nx ny x0 y0 dx dy)) 24 | (define label-templates-rectangular 25 | '( 26 | 27 | ;; File: /usr/share/glabels/templates/misc-us-templates.xml 28 | ( "OfficeMax OM99060" "Mailing Labels" ( 203.76 72.0 ) (3 10 0.0 36.0 203.76 72.0)) 29 | ( "Kingdom L" "Cassette Labels" ( 252.0 117.0 ) (2 6 33.12 27.0 261.0 126.144)) 30 | ( "Tough-Tags TTLW-2016" "Microtube labels" ( 92.16 36 ) (5 17 68 4.17 100.8 45.3)) 31 | ( "Netc 749303-70001 " "DLT Labels" ( 162 59.4 ) (3 10 36 45 189 72)) 32 | ( "Neato Slimline CD Case, spine" "Slimline CD Case (upside down)" ( 394.5 342.5 ) (1 2 96.5 41.9 0 363)) 33 | ( "Neato Slimline CD Case" "Slimline CD Case (rightside up)" ( 394.5 342.5 ) (1 2 130 41.9 0 363)) 34 | ( "Stomper PRO Spine" "PRO CD Labels 2-up (CD spine only)" ( 288 20 ) (2 1 18 385 288 0)) 35 | ( "Stomper PRO Zip" "PRO CD Labels 2-up (Face only)" ( 168 142 ) (1 2 407 68 0 142)) 36 | ( "Neato USCD2lbl Rectangles" "CD Template Rectangles" ( 77.04 234.36 ) (2 1 51.3 279.72 433.44 0)) 37 | ( "Southworth BC" "Business Cards" ( 252 144 ) (2 5 36 36 288 144)) 38 | 39 | ;; File: /usr/share/glabels/templates/avery-us-templates.xml 40 | ( "Avery LSK-3.5" "Divider Labels" ( 225.36 36.0 ) (1 20 36.0 36.0 315.36 36.0)) 41 | ( "Avery LSK-3" "Divider Labels" ( 225.36 36.0 ) (2 20 36.0 36.0 315.36 36.0)) 42 | ( "Avery LSK-5.5" "Divider Labels" ( 126.0 36.0 ) (2 20 36.0 21.6 147.6 36.0)) 43 | ( "Avery LSK-5" "Divider Labels" ( 126.0 36.0 ) (4 20 36.0 21.6 147.6 36.0)) 44 | ( "Avery LSK-8.5" "Divider Labels" ( 81.36 36.0 ) (2 20 36.0 36.0 153.36 36.0)) 45 | ( "Avery LSK-8" "Divider Labels" ( 81.36 36.0 ) (4 20 36.0 36.0 153.36 36.0)) 46 | ( "Avery 3274.1" "Square Labels" ( 180.0 180.0 ) (3 3 22.5 90.0 193.5 216.0)) 47 | ( "Avery 8165" "Full Sheet Labels" ( 612.0 792.0 ) (1 1 0 0 0 0)) 48 | ( "Avery 6570" "ID Labels" ( 126.0 90.0 ) (4 8 36.0 36.0 139.536 90.0)) 49 | ( "Avery 5997-Spine" "Video Tape Spine Labels" ( 414 48 ) (1 15 99 36 0 48)) 50 | ( "Avery 5997-Face" "Video Tape Face Labels" ( 220 133 ) (2 5 80 60.5 236 133)) 51 | ( "Avery 5931-Spine" "CD/DVD Labels (Spine Labels)" ( 15.75 337.5 ) (2 2 36.0 52.875 33.75 348.75)) 52 | ( "Avery 5395" "Name Badge Labels" ( 243.0 167.999999976 ) (2 4 49.5 41.999999976 270.0 180.0)) 53 | ( "Avery 8373" "Business Cards" ( 252.0 144.0 ) (2 4 36.0 54.0 288.0 180.0)) 54 | ( "Avery 5389" "Post cards" ( 432.0 288.0 ) (1 2 90.0 90.0 432.0 324.0)) 55 | ( "Avery 5388" "Index Cards" ( 360.0 216.0 ) (1 3 126.0 72.0 360.0 216.0)) 56 | ( "Avery 5371" "Business Cards" ( 252.0 144.0 ) (2 5 54.0 36.0 252.0 144.0)) 57 | ( "Avery 5366" "Filing Labels" ( 247.5 48.000000024 ) (2 15 38.25 36.0 288.0 48.000000024)) 58 | ( "Avery 6490" "Diskette Labels" ( 193.5 144.0 ) (3 5 9.0 36.0 200.25 144.0)) 59 | ( "Avery 5663" "Address Labels" ( 306.0 144.0 ) (2 5 0.0 36.0 306.0 144.0)) 60 | ( "Avery 5197" "Address Labels" ( 288.0 108.0 ) (2 6 13.5 72.0 297.0 108.0)) 61 | ( "Avery 5196" "Diskette Labels" ( 198.0 198.0 ) (3 3 9.0 36.0 198.0 216.0)) 62 | ( "Avery 5167" "Return Address Labels" ( 126.0 36.0 ) (4 20 20.25 36.0 148.5 36.0)) 63 | ( "Avery 5168" "Shipping Labels" ( 252.0 360.0 ) (2 2 36.0 36.0 288.0 360.0)) 64 | ( "Avery 5164" "Shipping Labels" ( 288.0 239.999999976 ) (2 3 11.25 36.0 301.5 239.999999976)) 65 | ( "Avery 5163" "Shipping Labels" ( 288.0 144.0 ) (2 5 11.7 36.0 301.5 144.0)) 66 | ( "Avery 5159" "Address Labels" ( 288.0 108.0 ) (2 7 11.25 18.0 301.5 108.0)) 67 | ( "Avery 6879" "Address Labels" ( 270.0 90.0 ) (2 6 27.0 81.0 288.0 108.0)) 68 | ( "Avery 5305" "Tent Cards" ( 612.0 180.0 ) (1 2 0.0 216.0 0.0 360.0)) 69 | ( "Avery 5162" "Address Labels" ( 288.0 95.999999976 ) (2 7 11.25 59.999999976 301.5 95.999999976)) 70 | ( "Avery 5161" "Address Labels" ( 288.0 72.0 ) (2 10 11.25 36.0 301.5 72.0)) 71 | ( "Avery 5160" "Address Labels" ( 189.0 72.0 ) (3 10 13.5 36.0 198.0 72.0)) 72 | ) ) 73 | 74 | ;; CD Labels: (name comment (radius hole) (nx ny x0 y0 dx dy)) 75 | (define label-templates-cd 76 | '( 77 | ("Avery 5931" "CD Labels, 2 per sheet" (166.5 58.5) (1 2 139.5 49.5 0 360)) 78 | ) ) 79 | 80 | ;; Paper sizes we recognize: 81 | ;; Note, this is currently somewhat bogus because the label templates are 82 | ;; specific to a particular paper size. 83 | (define page-sizes 84 | '( 85 | ("a4" 595.276 841.89) 86 | ("us-letter" 612 792) 87 | )) 88 | 89 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 90 | ;; END LABEL TEMPLATES 91 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 92 | 93 | 94 | -------------------------------------------------------------------------------- /gimp2/gimplabels/labeltemplates.scm-dist: -------------------------------------------------------------------------------- 1 | ; labeltemplates.scm: Templates for labels.scm. 2 | ; Copyright (C) 2005,2009 by Akkana Peck, akkana@shallowsky.com. 3 | ; 4 | ; This program is free software; you can redistribute it and/or modify 5 | ; it under the terms of the GNU General Public License as published by 6 | ; the Free Software Foundation; either version 2 of the License, or 7 | ; (at your option) any later version. 8 | ; 9 | ; This program is distributed in the hope that it will be useful, 10 | ; but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | ; GNU General Public License for more details. 13 | ; 14 | ; You should have received a copy of the GNU General Public License 15 | ; along with this program; if not, write to the Free Software 16 | ; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 17 | 18 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 19 | ;; LABEL TEMPLATES 20 | ;; Eventually these should move to a separate, auto-generated file. 21 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 22 | 23 | ;; Rectangular labels: (name comment (w h) (nx ny x0 y0 dx dy)) 24 | (define label-templates-rectangular 25 | '( 26 | 27 | ;; File: /usr/share/glabels/templates/misc-us-templates.xml 28 | ( "OfficeMax OM99060" "Mailing Labels" ( 203.76 72.0 ) (3 10 0.0 36.0 203.76 72.0)) 29 | ( "Kingdom L" "Cassette Labels" ( 252.0 117.0 ) (2 6 33.12 27.0 261.0 126.144)) 30 | ( "Tough-Tags TTLW-2016" "Microtube labels" ( 92.16 36 ) (5 17 68 4.17 100.8 45.3)) 31 | ( "Netc 749303-70001 " "DLT Labels" ( 162 59.4 ) (3 10 36 45 189 72)) 32 | ( "Neato Slimline CD Case, spine" "Slimline CD Case (upside down)" ( 394.5 342.5 ) (1 2 96.5 41.9 0 363)) 33 | ( "Neato Slimline CD Case" "Slimline CD Case (rightside up)" ( 394.5 342.5 ) (1 2 130 41.9 0 363)) 34 | ( "Stomper PRO Spine" "PRO CD Labels 2-up (CD spine only)" ( 288 20 ) (2 1 18 385 288 0)) 35 | ( "Stomper PRO Zip" "PRO CD Labels 2-up (Face only)" ( 168 142 ) (1 2 407 68 0 142)) 36 | ( "Neato USCD2lbl Rectangles" "CD Template Rectangles" ( 77.04 234.36 ) (2 1 51.3 279.72 433.44 0)) 37 | ( "Southworth BC" "Business Cards" ( 252 144 ) (2 5 36 36 288 144)) 38 | 39 | ;; File: /usr/share/glabels/templates/avery-us-templates.xml 40 | ( "Avery LSK-3.5" "Divider Labels" ( 225.36 36.0 ) (1 20 36.0 36.0 315.36 36.0)) 41 | ( "Avery LSK-3" "Divider Labels" ( 225.36 36.0 ) (2 20 36.0 36.0 315.36 36.0)) 42 | ( "Avery LSK-5.5" "Divider Labels" ( 126.0 36.0 ) (2 20 36.0 21.6 147.6 36.0)) 43 | ( "Avery LSK-5" "Divider Labels" ( 126.0 36.0 ) (4 20 36.0 21.6 147.6 36.0)) 44 | ( "Avery LSK-8.5" "Divider Labels" ( 81.36 36.0 ) (2 20 36.0 36.0 153.36 36.0)) 45 | ( "Avery LSK-8" "Divider Labels" ( 81.36 36.0 ) (4 20 36.0 36.0 153.36 36.0)) 46 | ( "Avery 3274.1" "Square Labels" ( 180.0 180.0 ) (3 3 22.5 90.0 193.5 216.0)) 47 | ( "Avery 8165" "Full Sheet Labels" ( 612.0 792.0 ) (1 1 0 0 0 0)) 48 | ( "Avery 6570" "ID Labels" ( 126.0 90.0 ) (4 8 36.0 36.0 139.536 90.0)) 49 | ( "Avery 5997-Spine" "Video Tape Spine Labels" ( 414 48 ) (1 15 99 36 0 48)) 50 | ( "Avery 5997-Face" "Video Tape Face Labels" ( 220 133 ) (2 5 80 60.5 236 133)) 51 | ( "Avery 5931-Spine" "CD/DVD Labels (Spine Labels)" ( 15.75 337.5 ) (2 2 36.0 52.875 33.75 348.75)) 52 | ( "Avery 5395" "Name Badge Labels" ( 243.0 167.999999976 ) (2 4 49.5 41.999999976 270.0 180.0)) 53 | ( "Avery 8373" "Business Cards" ( 252.0 144.0 ) (2 4 36.0 54.0 288.0 180.0)) 54 | ( "Avery 5389" "Post cards" ( 432.0 288.0 ) (1 2 90.0 90.0 432.0 324.0)) 55 | ( "Avery 5388" "Index Cards" ( 360.0 216.0 ) (1 3 126.0 72.0 360.0 216.0)) 56 | ( "Avery 5371" "Business Cards" ( 252.0 144.0 ) (2 5 54.0 36.0 252.0 144.0)) 57 | ( "Avery 5366" "Filing Labels" ( 247.5 48.000000024 ) (2 15 38.25 36.0 288.0 48.000000024)) 58 | ( "Avery 6490" "Diskette Labels" ( 193.5 144.0 ) (3 5 9.0 36.0 200.25 144.0)) 59 | ( "Avery 5663" "Address Labels" ( 306.0 144.0 ) (2 5 0.0 36.0 306.0 144.0)) 60 | ( "Avery 5197" "Address Labels" ( 288.0 108.0 ) (2 6 13.5 72.0 297.0 108.0)) 61 | ( "Avery 5196" "Diskette Labels" ( 198.0 198.0 ) (3 3 9.0 36.0 198.0 216.0)) 62 | ( "Avery 5167" "Return Address Labels" ( 126.0 36.0 ) (4 20 20.25 36.0 148.5 36.0)) 63 | ( "Avery 5168" "Shipping Labels" ( 252.0 360.0 ) (2 2 36.0 36.0 288.0 360.0)) 64 | ( "Avery 5164" "Shipping Labels" ( 288.0 239.999999976 ) (2 3 11.25 36.0 301.5 239.999999976)) 65 | ( "Avery 5163" "Shipping Labels" ( 288.0 144.0 ) (2 5 11.7 36.0 301.5 144.0)) 66 | ( "Avery 5159" "Address Labels" ( 288.0 108.0 ) (2 7 11.25 18.0 301.5 108.0)) 67 | ( "Avery 6879" "Address Labels" ( 270.0 90.0 ) (2 6 27.0 81.0 288.0 108.0)) 68 | ( "Avery 5305" "Tent Cards" ( 612.0 180.0 ) (1 2 0.0 216.0 0.0 360.0)) 69 | ( "Avery 5162" "Address Labels" ( 288.0 95.999999976 ) (2 7 11.25 59.999999976 301.5 95.999999976)) 70 | ( "Avery 5161" "Address Labels" ( 288.0 72.0 ) (2 10 11.25 36.0 301.5 72.0)) 71 | ( "Avery 5160" "Address Labels" ( 189.0 72.0 ) (3 10 13.5 36.0 198.0 72.0)) 72 | ) ) 73 | 74 | ;; CD Labels: (name comment (radius hole) (nx ny x0 y0 dx dy)) 75 | (define label-templates-cd 76 | '( 77 | ("Avery 5931" "CD Labels, 2 per sheet" (166.5 58.5) (1 2 139.5 49.5 0 360)) 78 | ) ) 79 | 80 | ;; Paper sizes we recognize: 81 | ;; Note, this is currently somewhat bogus because the label templates are 82 | ;; specific to a particular paper size. 83 | (define page-sizes 84 | '( 85 | ("a4" 595.276 841.89) 86 | ("us-letter" 612 792) 87 | )) 88 | 89 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 90 | ;; END LABEL TEMPLATES 91 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 92 | 93 | 94 | -------------------------------------------------------------------------------- /gimp2/gimplabels/make-label-fu.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | # Script to generate label templates from glabels XML template files, 4 | # for a GIMP label script-fu. 5 | # Copyright (C) 2005,2009 by Akkana Peck. 6 | # You are free to use, share or modify this program under 7 | # the terms of the GPL. 8 | 9 | import os, xml.dom.minidom, re, sys 10 | 11 | def handleTemplate(templ) : 12 | rect = templ.getElementsByTagName("Label-rectangle") 13 | if len(rect) <= 0 : return 14 | rect = rect[0] 15 | layout = rect.getElementsByTagName("Layout")[0] 16 | brand = templ.getAttribute("brand") 17 | part = templ.getAttribute("part") 18 | description = templ.getAttribute("description") 19 | if description == "" : 20 | description = templ.getAttribute("_description") 21 | #print "brand=", brand, "part=", part, "descript=", description 22 | name = " ".join([brand, part]) 23 | #print "becomes:", name 24 | 25 | # Markup margin: the glabels doc isn't clear what this is, 26 | # but I'm guessing it gets added around the edge of each label. 27 | # markupMargin = rect.getElementsByTagName("Markup-margin") 28 | # if len(markupMargin) > 0 : 29 | # margin = float(markupMargin[0].getAttribute("size"))*2 30 | # else : margin = 0 31 | 32 | # Try to add double the margin to the width and height. 33 | # But if the width or height is in a nontrivial format, 34 | # e.g. 3.13in, then skip it and just save the string. 35 | # try : 36 | # width = float(rect.getAttribute("width")) + margin 37 | # except ValueError, e: 38 | # width = rect.getAttribute("width") 39 | # try : 40 | # height = rect.getAttribute("height") 41 | # except ValueError, e: 42 | # height = float(rect.getAttribute("height")) + margin 43 | 44 | # glabels now specifies sizes and positions like ".75in" or "22pt". 45 | # So we need to parse the units and do something appropriate. 46 | # Return in points. 47 | def getNumAtt(node, attname) : 48 | lenstr = node.getAttribute(attname) 49 | unit = re.compile("[a-zA-Z]") 50 | #print "Searching for unit in", lenstr 51 | match = unit.search(lenstr) 52 | if not match : 53 | #print "No match in", lenstr 54 | return lenstr 55 | num = float(lenstr[0:match.start()]) 56 | unit = lenstr[match.start():] 57 | if unit == "pt" : 58 | return str(num) 59 | if unit == "in" : 60 | return str(num * 72) 61 | if unit == "mm" : 62 | return str(num * 2.8346457) 63 | if unit == "cm" : 64 | return str(num * 28.346457) 65 | 66 | # oops, no idea 67 | print ";; don't know the unit for", lenstr 68 | return str(num) 69 | 70 | width = getNumAtt(rect, "width") 71 | height = getNumAtt(rect, "height") 72 | 73 | # Print out the template, in script-fu format 74 | print " (", 75 | print "\"" + name + "\"", 76 | print "\"" + description + "\"", 77 | print "(", width, height, ")", 78 | print "(" + layout.getAttribute("nx"), layout.getAttribute("ny"), 79 | print getNumAtt(layout, "x0"), getNumAtt(layout, "y0"), 80 | print getNumAtt(layout, "dx"), getNumAtt(layout, "dy") + "))" 81 | 82 | def handleTemplateFile(tfdom) : 83 | templates = tfdom.getElementsByTagName("Template") 84 | # Loop over the templates in reverse, because script-fu (SIOD) 85 | # can't append to the end of a list only to the beginning; 86 | # so the template list will end up being the reverse of the 87 | # order in which we list them here. 88 | for i in range (len(templates)-1, -1, -1) : 89 | handleTemplate(templates[i]) 90 | 91 | def parseTemplateFile(fnam) : 92 | print "\n;; File:", fnam 93 | dom = xml.dom.minidom.parse(fnam) 94 | handleTemplateFile(dom) 95 | dom.unlink() 96 | 97 | print "; labeltemplates.scm: Templates for labels.scm.\n\ 98 | ; Copyright (C) 2005,2009 by Akkana Peck, akkana@shallowsky.com.\n\ 99 | ; \n\ 100 | ; This program is free software; you can redistribute it and/or modify\n\ 101 | ; it under the terms of the GNU General Public License as published by\n\ 102 | ; the Free Software Foundation; either version 2 of the License, or\n\ 103 | ; (at your option) any later version.\n\ 104 | ; \n\ 105 | ; This program is distributed in the hope that it will be useful,\n\ 106 | ; but WITHOUT ANY WARRANTY; without even the implied warranty of\n\ 107 | ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n\ 108 | ; GNU General Public License for more details.\n\ 109 | ; \n\ 110 | ; You should have received a copy of the GNU General Public License\n\ 111 | ; along with this program; if not, write to the Free Software\n\ 112 | ; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.\n\ 113 | \n\ 114 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\ 115 | ;; LABEL TEMPLATES\n\ 116 | ;; Eventually these should move to a separate, auto-generated file.\n\ 117 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\ 118 | \n\ 119 | ;; Rectangular labels: (name comment (w h) (nx ny x0 y0 dx dy))\n\ 120 | (define label-templates-rectangular\n\ 121 | '(" 122 | 123 | parseTemplateFile("/usr/share/glabels/templates/misc-us-templates.xml") 124 | parseTemplateFile("/usr/share/glabels/templates/avery-us-templates.xml") 125 | 126 | # Also parse anything in ~/.glabels 127 | mytemplatedir = os.environ["HOME"] + "/.glabels" 128 | try: 129 | mytemplatefiles = os.listdir(mytemplatedir) 130 | for templidx in range (0, len(mytemplatefiles)) : 131 | templ = mytemplatedir + "/" + mytemplatefiles[templidx] 132 | parseTemplateFile(templ) 133 | except OSError, e: 134 | print ";; No personal glabels templates:", e 135 | pass 136 | 137 | print " ) )\n\ 138 | \n\ 139 | ;; CD Labels: (name comment (radius hole) (nx ny x0 y0 dx dy))\n\ 140 | (define label-templates-cd\n\ 141 | '(\n\ 142 | (\"Avery 5931\" \"CD Labels, 2 per sheet\" (166.5 58.5) (1 2 139.5 49.5 0 360))\n\ 143 | ) )\n\ 144 | \n\ 145 | ;; Paper sizes we recognize:\n\ 146 | ;; Note, this is currently somewhat bogus because the label templates are\n\ 147 | ;; specific to a particular paper size.\n\ 148 | (define page-sizes\n\ 149 | '(\n\ 150 | (\"a4\" 595.276 841.89)\n\ 151 | (\"us-letter\" 612 792)\n\ 152 | ))\n\ 153 | \n\ 154 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\ 155 | ;; END LABEL TEMPLATES\n\ 156 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\ 157 | \n\ 158 | " 159 | -------------------------------------------------------------------------------- /gimp2/life.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # life.py: Play Conway's Game of Life on your image. 4 | 5 | # Copyright 2010 by Akkana Peck, http://www.shallowsky.com/software/ 6 | # You may use and distribute this plug-in under the terms of the GPL. 7 | # 8 | # Thanks to Joao Bueno for some excellent tips on speeding up 9 | # pixel ops in gimp-python! 10 | 11 | import math 12 | import time 13 | from gimpfu import * 14 | from array import array 15 | import sys # only needed for debugging 16 | 17 | # FACTOR should be 256. * 3. if you want to use only black and white. 18 | # Needs to be adjusted for colors, though. 19 | # And also for image type (currently assumes nchannels == 3). 20 | #FACTOR = 255 * 3. 21 | # Life rules, adjusted for RGB (each neighbor can contribute up to 255*3) 22 | NOTALIVE = 600 23 | LONELY = 1230 24 | NEWBORN = 1600 25 | OVERCROWDED = 2295 26 | 27 | NEWPXL = array("B", [ 180, 255, 180 ]) 28 | DYINGPXL = array("B", [ 80, 30, 30 ]) 29 | BLACK = array("B", [ 0, 0, 0 ]) 30 | 31 | def CA_rule(pixels, x, y, pos, width, height, nchannels=3) : 32 | oldval = pixels[pos : pos + nchannels] 33 | sumoldval = float(sum(oldval)) 34 | #print pos, sumoldval 35 | 36 | # Calculate new value based on Life rules 37 | neighbors = 0 38 | for xx in range(x-1, x+2) : 39 | for yy in range(y-1, y+2) : 40 | if xx == x and yy == y : continue # don't include self in sum 41 | dpos = (xx + width * yy) * nchannels 42 | s = int(sum(pixels[dpos : dpos + nchannels]) + .5) 43 | neighbors += s 44 | 45 | #print "(%d, %d): [%-3d %-3d %-3d]=%-3d, neighbors %-4d" % \ 46 | # (x, y, oldval[0], oldval[1], oldval[2], sumoldval, neighbors) 47 | #sys.stdout.flush() 48 | 49 | if neighbors < LONELY or neighbors > OVERCROWDED : 50 | # Pixel dies 51 | if sumoldval > NOTALIVE : 52 | return DYINGPXL 53 | else : 54 | # It is so ridiculous that python arrays can't handle 55 | # mathematical ops like oldval / 2 56 | return array("B", [ oldval[0]/2, oldval[1]/2, oldval[2]/2 ]) 57 | 58 | elif sumoldval < NOTALIVE and neighbors > NEWBORN : 59 | # New one born 60 | return NEWPXL 61 | 62 | else : 63 | # pixel stays the same 64 | return oldval 65 | 66 | def python_life_step(img, layer) : 67 | sys.stdout.flush() 68 | gimp.progress_init("Playing life ...") 69 | pdb.gimp_image_undo_group_start(img) 70 | 71 | width, height = layer.width, layer.height 72 | 73 | src_rgn = layer.get_pixel_rgn(0, 0, width, height, False, False) 74 | nchannels = len(src_rgn[0,0]) 75 | src_pixels = array("B", src_rgn[0:width, 0:height]) 76 | 77 | dst_rgn = layer.get_pixel_rgn(0, 0, width, height, True, True) 78 | dst_pixels = array("B", "\x00" * (width * height * nchannels)) 79 | 80 | # Loop over the region: 81 | for x in xrange(0, width - 1) : 82 | for y in xrange(0, height) : 83 | pos = (x + width * y) * nchannels 84 | dst_pixels[pos : pos + nchannels] = CA_rule(src_pixels, x, y, pos, 85 | width, height, 86 | nchannels) 87 | 88 | progress = float(x)/layer.width 89 | if (int(progress * 100) % 20 == 0) : 90 | gimp.progress_update(progress) 91 | 92 | # Copy the whole array back to the pixel region: 93 | dst_rgn[0:width, 0:height] = dst_pixels.tostring() 94 | 95 | layer.flush() 96 | layer.merge_shadow(True) 97 | layer.update(0, 0, width, height) 98 | 99 | pdb.gimp_image_undo_group_end(img) 100 | pdb.gimp_progress_end() 101 | 102 | pdb.gimp_displays_flush() 103 | 104 | import gtk 105 | 106 | class LifeWindow(gtk.Window): 107 | def __init__ (self, img, layer, *args): 108 | self.running = False 109 | self.img = img 110 | self.layer = layer 111 | win = gtk.Window.__init__(self, *args) 112 | self.set_border_width(10) 113 | 114 | # Obey the window manager quit signal: 115 | self.connect("destroy", gtk.main_quit) 116 | 117 | # Make the UI 118 | vbox = gtk.VBox(spacing=10, homogeneous=True) 119 | self.add(vbox) 120 | label = gtk.Label("Conway's Game of Life") 121 | vbox.add(label) 122 | label.show() 123 | hbox = gtk.HBox(spacing=20) 124 | 125 | btn = gtk.Button("Run") 126 | hbox.add(btn) 127 | btn.show() 128 | btn.connect("clicked", self.runstoplife) 129 | 130 | btn = gtk.Button("Single step") 131 | hbox.add(btn) 132 | btn.show() 133 | btn.connect("clicked", self.steplife) 134 | 135 | btn = gtk.Button("Cancel") 136 | hbox.add(btn) 137 | btn.show() 138 | btn.connect("clicked", gtk.main_quit) 139 | 140 | vbox.add(hbox) 141 | hbox.show() 142 | vbox.show() 143 | self.show() 144 | return win 145 | 146 | def steplife(self, widget) : 147 | python_life_step(self.img, self.layer) 148 | 149 | def runstoplife(self, widget) : 150 | if self.running : 151 | widget.set_label("Run") 152 | self.running = False 153 | return 154 | self.running = True 155 | widget.set_label("Stop") 156 | 157 | while self.running : 158 | python_life_step(self.img, self.layer) 159 | # Handle any button presses that have happened while we were busy 160 | while gtk.events_pending() : 161 | gtk.main_iteration() 162 | 163 | def python_life(img, layer) : 164 | l = LifeWindow(img, layer) 165 | gtk.main() 166 | 167 | register( 168 | "python_fu_life", 169 | "Conway's game of Life", 170 | "Conway's game of Life", 171 | "Akkana Peck", 172 | "Akkana Peck", 173 | "2010", 174 | "/Filters/Map/Life...", 175 | "RGB", # Adjust for other types when code is more flexible 176 | [ 177 | ], 178 | [], 179 | python_life) 180 | 181 | main() 182 | -------------------------------------------------------------------------------- /gimp2/migrate-gimp-presets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, re 4 | from gimpfu import * 5 | 6 | def python_fu_migrate_tool_presets() : 7 | tool_options_dir_2_6 = os.path.join(re.sub('2.8', '2.6', gimp.directory), 8 | 'tool-options') 9 | tool_preset_dir_2_8 = os.path.join(gimp.directory, 'tool-presets') 10 | 11 | presetfiles = os.listdir(tool_options_dir_2_6) 12 | 13 | for filename in presetfiles : 14 | # filename is something like gimp-rect-select-tool.presets 15 | # So the part before the extension is the tool name. 16 | 17 | f = open(os.path.join(tool_options_dir_2_6, filename)) 18 | toolname = os.path.splitext(filename)[0] 19 | optionsname = None 20 | presetname = None 21 | parenlevel = 0 22 | cur_preset_str = '' 23 | for line in f : 24 | left_parens = len(re.findall("\(", line)) 25 | right_parens = len(re.findall("\)", line)) 26 | 27 | if parenlevel <= 0 and left_parens <= 0 : 28 | # not in an s-expression, nothing to do 29 | parenlevel += left_parens - right_parens 30 | continue 31 | 32 | # Are we finishing an existing preset? Then write it. 33 | if parenlevel > 0 and parenlevel + left_parens - right_parens == 0 : 34 | cur_preset_str += line 35 | 36 | outf = open(os.path.join(tool_preset_dir_2_8, presetname) + '.gtp', 37 | 'w') 38 | print >>outf, '# GIMP tool preset file, migrated from 2.6\n' 39 | print >>outf, '(stock-id "%s")' % toolname 40 | print >>outf, '(name "%s")' % presetname 41 | print >>outf, '(tool-options "%s"' % optionsname 42 | print >>outf, cur_preset_str 43 | print >>outf, '# end of GIMP tool preset file' 44 | outf.close() 45 | 46 | cur_preset_str = '' 47 | presetname = None 48 | parenlevel = 0 49 | continue 50 | 51 | # Are we just continuing an existing preset? 52 | if parenlevel > 0 : 53 | cur_preset_str += line 54 | parenlevel += left_parens - right_parens 55 | continue 56 | 57 | # We're starting a new preset. Find its name: 58 | match = re.match('\W*(\w*?Options)\s*[\"\'](.*)[\"\'].*', line) 59 | if not match : 60 | continue 61 | optionsname = match.group(1) 62 | presetname = match.group(2) 63 | 64 | parenlevel += left_parens - right_parens 65 | 66 | f.close() 67 | 68 | register( 69 | "python_fu_migrate_tool_presets", 70 | "Migrate 2.6 tool presets to 2.8", 71 | "Migrate 2.6 tool presets to 2.8", 72 | "Akkana Peck", 73 | "Akkana Peck", 74 | "2012", 75 | "Migrate 2.6 Tool Presets...", 76 | "", 77 | [], 78 | [], 79 | python_fu_migrate_tool_presets, menu="/File") 80 | 81 | main() 82 | -------------------------------------------------------------------------------- /gimp2/migrate-plugins-2-99.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # A little script to take plug-ins that GIMP 2.99 incorrectly put 4 | # in ~/.config/GIMP/2.99/plug-ins/ and create the subdirectory for 5 | # each plug-in that GIMP now requires before it will see them. 6 | # Run this after you've run GIMP 2.99 for the first time and noticed 7 | # that none of your plug-ins are there. 8 | 9 | import os 10 | import shutil 11 | import tempfile 12 | 13 | new_plugins_dir = os.path.expanduser("~/.config/GIMP/2.99/plug-ins/") 14 | 15 | def make_new_plugin(oldpath): 16 | fname = os.path.basename(oldpath) 17 | base, ext = os.path.splitext(fname) 18 | 19 | # If the plug-in has an extension, like foo.py, 20 | # it will end up in foo/foo.py. 21 | # But if it has no extension, the directory name will be 22 | # the same as the plug-in name. 23 | if not ext: 24 | plugindir = tempfile.mkdtemp(prefix=os.path.join(new_plugins_dir, base)) 25 | print("Made temporary directory", plugindir) 26 | else: 27 | plugindir = os.path.join(new_plugins_dir, base) 28 | os.mkdir(plugindir) 29 | print("Made directory", plugindir) 30 | 31 | newpath = os.path.join(plugindir, fname) 32 | 33 | # If it's something GIMP has already copied, move it. 34 | # If it's from somewhere else, just copy it. 35 | if oldpath.startswith(new_plugins_dir): 36 | print("Rename", oldpath, newpath) 37 | os.rename(oldpath, newpath) 38 | else: 39 | print("COPY", oldpath, newpath) 40 | shutil.copy(oldpath, newpath) 41 | 42 | # If we had to make a temporary directory before, move it now 43 | # back to the original name: 44 | if ext: 45 | print("Renaming", plugindir, os.path.join(new_plugins_dir, base)) 46 | os.rename(plugindir, os.path.join(new_plugins_dir, base)) 47 | 48 | def move_migrated_plugins(): 49 | for f in os.listdir(new_plugins_dir): 50 | fullf = os.path.join(new_plugins_dir, f) 51 | if not os.path.isfile(fullf): 52 | continue 53 | make_new_plugin(fullf) 54 | 55 | if __name__ == '__main__': 56 | move_migrated_plugins() 57 | 58 | -------------------------------------------------------------------------------- /gimp2/pandora-combine.scm: -------------------------------------------------------------------------------- 1 | ;; pandora-combine.scm: Move layers around to make a panorama. 2 | ;; Copyright (C) 2006 by Akkana Peck, akkana@shallowsky.com. 3 | ;; 4 | ;; This program is free software; you can redistribute it and/or modify 5 | ;; it under the terms of the GNU General Public License. 6 | 7 | ;; Pandora Combine: 8 | ;; Start by opening all the images of your panorama as separate 9 | ;; layers in a single image, with the bottom layer being the leftmost. 10 | ;; Then run pandora-combine on that image. 11 | 12 | (define (script-fu-pandora-combine img drawable 13 | overlap top-on-right use-mask) 14 | ;; Calculate the size for the new image 15 | (let* ((layers (gimp-image-get-layers img)) 16 | (num-layers (car layers)) 17 | (layer-array (cadr layers)) 18 | (bottomlayer (aref layer-array (- num-layers 1))) 19 | ; Pandora assumes that all layers are the same size as the first: 20 | ; XXX change this eventually. 21 | (layer-w (car (gimp-drawable-width bottomlayer))) 22 | (layer-h (car (gimp-drawable-height bottomlayer))) 23 | (overlap-frac (/ overlap 100)) 24 | (extra-frac (- 1.0 overlap-frac)) 25 | (hslop (/ layer-w 4)) 26 | (vslop (/ (* hslop 3) 2)) 27 | (pan-img-w (* layer-w (+ 1 (* (- num-layers 0.3) (- 1 overlap-frac))))) 28 | (pan-img-h (+ layer-h vslop)) 29 | (newy (/ vslop 2)) 30 | (i (- num-layers 1) 1) ; start from the bottom layer 31 | ) 32 | ;(gimp-message (number->string pan-img-w)) 33 | (gimp-image-undo-group-start img) 34 | (gimp-image-resize img pan-img-w pan-img-h 0 0) 35 | 36 | ;; Loop over the layers starting with the second, moving each one. 37 | ;; Layers are numbered starting with 0 as the top layer in the stack. 38 | ;(gimp-layer-translate bottomlayer 0 newy) 39 | (gimp-context-push) 40 | (while (>= i 0) 41 | ;(gimp-message (number->string i)) 42 | (let* ((thislayer (aref layer-array i)) 43 | (thislayer-w (car (gimp-drawable-width thislayer))) 44 | (newx (if (= top-on-right TRUE) 45 | (* (- (- num-layers i) 1) 46 | (* thislayer-w extra-frac)) 47 | (* i (* thislayer-w extra-frac)) 48 | )) 49 | ) 50 | (if (= (car (gimp-layer-is-floating-sel thislayer)) FALSE) 51 | (begin 52 | (gimp-layer-translate thislayer newx newy) 53 | (if (and (= use-mask TRUE) 54 | (= (car (gimp-layer-get-mask thislayer)) -1) 55 | (not (= i (- num-layers 1)))) 56 | (let* ((masklayer (car (gimp-layer-create-mask 57 | thislayer ADD-BLACK-MASK))) 58 | (grad-w (* (* layer-w overlap-frac) 0.5)) 59 | (grad-start (if (= top-on-right TRUE) 60 | grad-w 61 | (- thislayer-w grad-w))) 62 | (grad-end (if (= top-on-right TRUE) 63 | 0 thislayer-w)) 64 | ) 65 | (gimp-layer-add-alpha thislayer) 66 | (gimp-layer-add-mask thislayer masklayer) 67 | (gimp-context-set-foreground '(255 255 255)) 68 | (gimp-context-set-background '(0 0 0)) 69 | (gimp-edit-blend masklayer FG-BG-RGB-MODE NORMAL-MODE 70 | GRADIENT-LINEAR 100 0 REPEAT-NONE FALSE 71 | FALSE 0 0 TRUE 72 | grad-start 0 grad-end 0) 73 | (gimp-layer-set-edit-mask thislayer FALSE) 74 | )))) 75 | ) 76 | (set! i (- i 1)) 77 | ) 78 | (gimp-context-pop) 79 | (gimp-image-undo-group-end img) 80 | (gimp-displays-flush) 81 | ) 82 | ) 83 | 84 | (if (symbol-bound? 'script-fu-menu-register (the-environment)) 85 | (begin 86 | (script-fu-register "script-fu-pandora-combine" 87 | _"Arrange as Panorama..." 88 | _"Line up layers as a panorama" 89 | "Akkana Peck" 90 | "Akkana Peck" 91 | "June 2006" 92 | "*" 93 | SF-IMAGE "Image" 0 94 | SF-DRAWABLE "Drawable" 0 95 | SF-ADJUSTMENT _"Overlap (percent)" '(50 0 100 1 10 0 1) 96 | SF-TOGGLE _"Top Layer on Right" TRUE 97 | SF-TOGGLE _"Use Layer Masks" TRUE 98 | ) 99 | 100 | (script-fu-menu-register "script-fu-pandora-combine" 101 | _"/Filters/Pandora") 102 | ) ; end begin 103 | (script-fu-register "script-fu-pandora-combine" 104 | _"/Filters/Pandora/Arrange as Panorama..." 105 | _"Line up layers as a panorama" 106 | "Akkana Peck" 107 | "Akkana Peck" 108 | "June 2006" 109 | "*" 110 | SF-IMAGE "Image" 0 111 | SF-DRAWABLE "Drawable" 0 112 | SF-ADJUSTMENT _"Overlap (percent)" '(50 0 100 1 10 0 1) 113 | SF-TOGGLE _"Top Layer on Right" TRUE 114 | SF-TOGGLE _"Use Layer Masks" TRUE 115 | ) 116 | ) 117 | 118 | -------------------------------------------------------------------------------- /gimp2/pyui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # GIMP Python plug-in template showing all UI elements. 4 | # Copyright 2010 Akkana Peck 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | # 20 | # Note: gimpfu.py is the most up-to-date list of UI items. 21 | 22 | from gimpfu import * 23 | 24 | def show_py_ui(number, angle, word, text, bgcolor, 25 | newimg, newlayer, channel, drawable, 26 | shadowp, ascendingp, imgfmt, option, 27 | size, opacity, imagefile, dir, 28 | font, brush, pattern, gradient, palette ) : 29 | return 30 | 31 | register( 32 | "python_fu_ui_options", 33 | "Show all Python-Fu UI options", 34 | "Longer description of doing stuff", 35 | "Your Name", 36 | "Your Name", 37 | "2010", 38 | "Show UI Options...", 39 | "", # Alternately use RGB, RGB*, GRAY*, INDEXED etc. 40 | [ 41 | (PF_INT, "number", "Number?", 50), 42 | (PF_FLOAT, "angle", "Angle", 3.14159), 43 | # you can also use PF_INT8, PF_INT16, PF_INT32 44 | (PF_STRING, "word", "Word", "Zebrafish!"), 45 | # PF_VALUE is another term for PF_STRING 46 | (PF_TEXT, "text", "Some Text", 47 | "The quick red fox jumped over the lazy dog"), 48 | 49 | (PF_COLOR, "bg-color", "Background", (1.0, 0.0, 0.0)), 50 | # or you can spell it PF_COLOUR 51 | 52 | (PF_IMAGE, "image", "Input image", None), 53 | (PF_LAYER, "layer", "Input layer", None), 54 | (PF_CHANNEL, "channel", "Which channel", None), 55 | (PF_DRAWABLE, "drawable", "Input drawable", None), 56 | 57 | (PF_TOGGLE, "shadow", "Shadow?", 1), 58 | (PF_BOOL, "ascending", "_Ascending", True), 59 | (PF_RADIO, "imagefmt", "Image format", "jpg", 60 | (("png", "png"), ("jpg", "jpg"))), 61 | (PF_OPTION, "option", "Option", 2, ("Mouse", "Cat", "Dog", "Horse")), 62 | 63 | (PF_SPINNER, "size", "Pixel Size", 50, (1, 8000, 1)), 64 | (PF_SLIDER, "opacity", "Op_acity", 100, (0, 100, 1)), 65 | # (PF_ADJUSTMENT is the same as PF_SPINNER 66 | 67 | (PF_FILE, "imagefile", "Image file", ""), 68 | (PF_DIRNAME, "dir", "Directory", "/tmp"), 69 | 70 | (PF_FONT, "font", "Font", "Sans"), 71 | (PF_BRUSH, "brush", "Brush", None), 72 | (PF_PATTERN, "pattern", "Pattern", None), 73 | (PF_GRADIENT, "gradient", "Gradient", None), 74 | (PF_PALETTE, "palette", "Palette", ""), 75 | 76 | # New items that don't quite work yet: 77 | #(PF_VECTORS, "vectors", "Vectors", None), 78 | #(PF_DISPLAY, "display", "Display", None), 79 | ], 80 | [], 81 | show_py_ui, menu="/Filters/Languages/Python-Fu" ) 82 | 83 | main() 84 | -------------------------------------------------------------------------------- /gimp2/saver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This is a plug-in intended to be bound to Ctrl-S, and to act as 4 | # a combination of Save, Save as..., Overwrite, and Export... 5 | # 6 | # It saves the image (whether its type would normally require Saving 7 | # or Exporting), and optionally allows for a copy of the image to be 8 | # exported to another file, possibly also scaled differently. 9 | # The copy must be saved to the same directory as the original 10 | # (because juggling two file choosers would make the dialog too huge 11 | # to fit on most screens!) 12 | # 13 | # Regardless of image format chosen, the image will be marked clean 14 | # so you won't get prompted for an unsaved image at quit or close time. 15 | # 16 | # The first time you save a file, you should be prompted for filename 17 | # (even if you opened an existing file). 18 | # After that, the plug-in will remember the filename you set. 19 | # You can always change it with Saver as... 20 | # 21 | # So you can have your XCF with high resolution and all layers, 22 | # and automatically export to a quarter-scale JPEG copy every time 23 | # you save the original. Or you can just overwrite your JPG if 24 | # all you're doing is a quick edit. 25 | # 26 | # The basic save filename follows img.filename. 27 | # The copy, if any, is saved in a parasite: 28 | # export-scaled: filename\npercent\nwidth\nheight 29 | 30 | # This plug-in is hosted at https://github.com/akkana/gimp-plugins/blob/master/saver.py 31 | 32 | # Copyright 2013,2014 by Akkana Peck, http://www.shallowsky.com/software/ 33 | # You may use and distribute this plug-in under the terms of the GPL v2 34 | # or, at your option, any later GPL version. 35 | 36 | from gimpfu import * 37 | import gtk 38 | import os 39 | import collections 40 | 41 | # Image original size 42 | orig_width = 0 43 | orig_height = 0 44 | 45 | # The three gtk.Entry widgets we'll monitor: 46 | percent_e = None 47 | width_e = None 48 | height_e = None 49 | 50 | # A way to show errors to the user: 51 | warning_label = None 52 | 53 | # A way to turn off notifications while we're calculating 54 | busy = False 55 | 56 | def save_both(img, drawable, filename, copyname, width, height): 57 | '''Save the image, and also save the copy if appropriate, 58 | doing any duplicating or scaling that might be necessary. 59 | Returns None on success, else a nonempty string error message. 60 | ''' 61 | msg = "Saving " + filename 62 | 63 | if copyname: 64 | if width and height: 65 | msg += " and a scaled copy, " + copyname 66 | else: 67 | msg += " and a copy, " + copyname 68 | print msg 69 | # Don't use gimp_message -- it can pop up a dialog. 70 | # pdb.gimp_message_set_handler(MESSAGE_BOX) 71 | # pdb.gimp_message(msg) 72 | # Alternately, could use gimp_progress, though that's not ideal. 73 | # If you use gimp_progress, don't forget to call pdb.gimp_progress_end() 74 | #pdb.gimp_progress_set_text(msg) 75 | 76 | # Is there any point to pushing and popping the context? 77 | #gimp.context_pop() 78 | 79 | # Set up the parasite with information about the copied image, 80 | # so it will be saved with the main XCF image. 81 | if copyname: 82 | if (width and height): 83 | percent = width * 100.0 / img.width 84 | # XXX note that this does not guard against changed aspect ratios. 85 | parastring = '%s\n%d\n%d\n%d' % (copyname, percent, width, height) 86 | else: 87 | parastring = '%s\n100.0\nNone\nNone' % (copyname) 88 | 89 | print "Saving parasite", parastring 90 | para = img.attach_new_parasite('export-copy', 1, parastring) 91 | # The second argument is is_persistent 92 | 93 | # Also make sure that copyname is a full path (if filename is). 94 | copypath = os.path.split(copyname) 95 | if not copypath[0] or len(copypath) == 1: 96 | filepath = os.path.split(filename) 97 | if len(filepath) > 1 and filepath[0]: 98 | print "Turning", copyname, "into a full path:", 99 | copyname = os.path.join(filepath[0], copyname) 100 | print copyname 101 | 102 | # Now the parasite is safely attached, and we can save. 103 | # Alas, we can't attach the JPEG settings parasite until after 104 | # we've saved the copy, so that won't get saved with the main image 105 | # though it will be attached to the image and remembered in 106 | # this session, and the next time we save it should be remembered. 107 | 108 | def is_xcf(thefilename): 109 | base, ext = os.path.splitext(thefilename) 110 | ext = ext.lower() 111 | if ext == '.gz' or ext == '.bz2': 112 | base, ext = os.path.splitext(base) 113 | ext = ext.lower() 114 | return (ext == '.xcf') 115 | 116 | # With the new "crop doesn't really crop", gimp_file_save saves the 117 | # whole image, not the cropped version. 118 | # So detect cropping or anything else that might make layers 119 | # be sized differently from the image. 120 | def different_size_layer(img): 121 | for l in img.layers: 122 | if l.width != img.width: 123 | return True 124 | if l.height != img.height: 125 | return True 126 | return False 127 | 128 | # First, save the original image. 129 | if is_xcf(filename) or (len(img.layers) < 2 130 | and not different_size_layer(img)): 131 | pdb.gimp_file_save(img, drawable, filename, filename) 132 | else: 133 | # It's not XCF and it has multiple layers, 134 | # or one layer that's been cropped. 135 | # We need to make a new image and flatten it. 136 | copyimg = pdb.gimp_image_duplicate(img) 137 | # Don't actually flatten since that will prevent saving transparent png. 138 | #copyimg.flatten() 139 | pdb.gimp_image_merge_visible_layers(copyimg, CLIP_TO_IMAGE) 140 | pdb.gimp_file_save(copyimg, copyimg.active_layer, filename, filename) 141 | gimp.delete(copyimg) 142 | 143 | # We've done the important part, so mark the image clean. 144 | img.filename = filename 145 | pdb.gimp_image_clean_all(img) 146 | 147 | # If we don't have to save a copy, return. 148 | if not copyname: 149 | return None 150 | 151 | # We'll need a new image if the copy is non-xcf and we have more 152 | # than one layer, or if we're scaling. 153 | if (width and height): 154 | # We're scaling! 155 | copyimg = pdb.gimp_image_duplicate(img) 156 | copyimg.scale(width, height) 157 | print "Scaling to", width, 'x', height 158 | print "Set parastring to", parastring, "(end of parastring)" 159 | 160 | elif len(img.layers) > 1 and not is_xcf(copyname): 161 | # We're not scaling, but we still need to flatten. 162 | copyimg = pdb.gimp_image_duplicate(img) 163 | # copyimg.flatten() 164 | pdb.gimp_image_merge_visible_layers(copyimg, CLIP_TO_IMAGE) 165 | print "Flattening but not scaling" 166 | else: 167 | copyimg = img 168 | print "Not scaling or flattening" 169 | 170 | # gimp-file-save insists on being passed a valid layer, 171 | # even if saving to a multilayer format such as XCF. Go figure. 172 | 173 | # I don't get it. What's the rule for whether gimp_file_save 174 | # will prompt for jpg parameters? Saving back to an existing 175 | # file doesn't seem to help. 176 | # run_mode=RUN_WITH_LAST_VALS is supposed to prevent prompting, 177 | # but actually seems to do nothing. Copying the -save-options 178 | # parasites is more effective. 179 | try: 180 | pdb.gimp_file_save(copyimg, copyimg.active_layer, copyname, copyname, 181 | run_mode=RUN_WITH_LAST_VALS) 182 | except RuntimeError, e: 183 | gimp.delete(copyimg) 184 | print "Runtime error -- didn't save" 185 | return "Runtime error -- didn't save" 186 | 187 | # Find any image type settings parasites (e.g. jpeg settings) 188 | # that got set during save, so we'll be able to use them 189 | # next time. 190 | def copy_settings_parasites(fromimg, toimg): 191 | for pname in fromimg.parasite_list(): 192 | if pname[-9:] == '-settings' or pname[-13:] == '-save-options': 193 | para = fromimg.parasite_find(pname) 194 | if para: 195 | toimg.attach_new_parasite(pname, para.flags, para.data) 196 | 197 | # Copy any settings parasites we may have saved from previous runs: 198 | copy_settings_parasites(copyimg, img) 199 | 200 | gimp.delete(copyimg) 201 | #gimp.Display(copyimg) 202 | return None 203 | 204 | def init_from_parasite(img): 205 | '''Returns copyname, percent, width, height.''' 206 | para = img.parasite_find('export-copy') 207 | if para: 208 | #copyname, percent, width, height = \ 209 | paravals = \ 210 | map(lambda x: 0 if x == 'None' or x == '' else x, 211 | para.data.split('\n')) 212 | copyname = paravals[0] 213 | percent = float(paravals[1]) 214 | width = int(paravals[2]) 215 | height = int(paravals[3]) 216 | print "Read parasite values", copyname, percent, width, height 217 | return copyname, percent, width, height 218 | else: 219 | return None, 100.0, img.width, img.height 220 | 221 | def python_fu_saver(img, drawable): 222 | # Figure out whether this image already has filenames chosen; 223 | # if so, just save and export; 224 | # if not, call python_fu_saver_as(img, drawable). 225 | 226 | # Figure out whether there's an export parasite 227 | # so we can pass width and height to save_both. 228 | copyname, export_percent, export_width, export_height = \ 229 | init_from_parasite(img) 230 | 231 | if img.filename: 232 | save_both(img, drawable, img.filename, copyname, 233 | export_width, export_height) 234 | else: 235 | # If we don't have a filename, either from the current session 236 | # or saved as a parasite, then we'd better prompt for one. 237 | python_fu_saver_as(img, drawable) 238 | 239 | def python_fu_saver_as(img, drawable): 240 | global percent_e, width_e, height_e, orig_width, orig_height, warning_label 241 | orig_filename = img.filename 242 | 243 | orig_width = img.width 244 | orig_height = img.height 245 | 246 | # Do we already have defaults? 247 | copyname, export_percent, export_width, export_height = \ 248 | init_from_parasite(img) 249 | 250 | chooser = gtk.FileChooserDialog(title=None, 251 | action=gtk.FILE_CHOOSER_ACTION_SAVE, 252 | buttons=(gtk.STOCK_CANCEL, 253 | gtk.RESPONSE_CANCEL, 254 | gtk.STOCK_SAVE, 255 | gtk.RESPONSE_OK)) 256 | 257 | # Set a current folder, since otherwise it'll be empty 258 | # and will nag the user to set one. 259 | if img.filename: 260 | chooser.set_current_name(os.path.basename(img.filename)) 261 | chooser.set_current_folder(os.path.dirname(img.filename)) 262 | else: 263 | # No directory set yet. So pick a likely directory 264 | # that's used by a lot of images currently open. 265 | counts = collections.Counter() 266 | for i in gimp.image_list() : 267 | if i.filename : 268 | counts[os.path.dirname(i.filename)] += 1 269 | try : 270 | common = counts.most_common(1)[0][0] 271 | chooser.set_current_folder(common) 272 | except : 273 | # GIMP has no images open with pathnames associated. 274 | # So we can't guess at a directory; just leave it blank. 275 | pass 276 | 277 | copybox = gtk.Table(rows=3, columns=7) 278 | 279 | l = gtk.Label("Export a copy to:") 280 | l.set_alignment(1.0, 0.5) # Right align 281 | copybox.attach(l, 0, 1, 0, 1, 282 | xpadding=5, ypadding=5) 283 | 284 | copyname_e = gtk.Entry() 285 | if copyname: 286 | copyname_e.set_text(copyname) 287 | copybox.attach(copyname_e, 1, 7, 0, 1, 288 | xpadding=5, ypadding=5) 289 | 290 | l = gtk.Label("Scale the copy:") 291 | l.set_alignment(1.0, 0.5) 292 | copybox.attach(l, 0, 1, 1, 2, 293 | xpadding=5, ypadding=5) 294 | 295 | l = gtk.Label("Percent:") 296 | l.set_alignment(1.0, 0.5) 297 | copybox.attach(l, 1, 2, 1, 2, 298 | xpadding=5, ypadding=5) 299 | 300 | adj = gtk.Adjustment(int(export_percent), 1, 10000, 1, 10, 0) 301 | percent_e = gtk.SpinButton(adj, 0, 0) 302 | percent_e.connect("changed", entry_changed, 'p'); 303 | copybox.attach(percent_e, 2, 3, 1, 2, 304 | xpadding=5, ypadding=5) 305 | 306 | l = gtk.Label("Width:") 307 | l.set_alignment(1.0, 0.5) 308 | copybox.attach(l, 3, 4, 1, 2, 309 | xpadding=5, ypadding=5) 310 | 311 | adj = gtk.Adjustment(int(export_width), 1, 10000, 1, 10, 0) 312 | width_e = gtk.SpinButton(adj, 0, 0) 313 | width_e.connect("changed", entry_changed, 'w'); 314 | copybox.attach(width_e, 4, 5, 1, 2, 315 | xpadding=5, ypadding=5) 316 | 317 | l = gtk.Label("Height:") 318 | l.set_alignment(1.0, 0.5) 319 | copybox.attach(l, 5, 6, 1, 2, 320 | xpadding=5, ypadding=5) 321 | 322 | adj = gtk.Adjustment(int(export_height), 1, 10000, 1, 10, 0) 323 | height_e = gtk.SpinButton(adj, 0, 0) 324 | height_e.connect("changed", entry_changed, 'h'); 325 | copybox.attach(height_e, 6, 7, 1, 2, 326 | xpadding=5, ypadding=5) 327 | 328 | warning_label = gtk.Label("") 329 | copybox.attach(warning_label, 0, 7, 2, 3, 330 | xpadding=5, ypadding=5) 331 | 332 | copybox.show_all() 333 | chooser.set_extra_widget(copybox) 334 | 335 | # Oh, cool, we could have shortcuts to image folders, 336 | # and maybe remove the stupid fstab shortcuts GTK adds for us. 337 | #chooser.add_shortcut_folder(folder) 338 | #chooser.remove_shortcut_folder(folder) 339 | 340 | # Loop to catch errors/warnings: 341 | while True: 342 | response = chooser.run() 343 | if response != gtk.RESPONSE_OK: 344 | chooser.destroy() 345 | return 346 | 347 | try: 348 | percent = float(percent_e.get_text()) 349 | except ValueError: 350 | percent = None 351 | try: 352 | width = int(width_e.get_text()) 353 | except ValueError: 354 | width = None 355 | try: 356 | height = int(height_e.get_text()) 357 | except ValueError: 358 | height = None 359 | 360 | filename = chooser.get_filename() 361 | copyname = copyname_e.get_text() 362 | 363 | if copyname == orig_filename: 364 | warning_label.set_text("Change the name or the directory -- don't overwrite original file!") 365 | continue 366 | 367 | # Whew, it's not the original filename, so now we can save. 368 | dpy = chooser.get_display() 369 | chooser.hide() 370 | dpy.sync() 371 | 372 | err = save_both(img, drawable, filename, copyname, width, height) 373 | if err: 374 | markup = '' 375 | markup_end = '' 376 | warning_label.set_markup(markup + "Didn't save to " + filename 377 | + ":" + str(e) + markup_end) 378 | warning_label.set_text(err) 379 | print "Error:", err 380 | chooser.show() 381 | continue 382 | else: 383 | pdb.gimp_image_clean_all(img) 384 | 385 | # Otherwise, save_both was happy so we can continue. 386 | chooser.destroy() 387 | 388 | #gimp.context_pop() 389 | 390 | return 391 | 392 | # Doesn't work to compare entry against percent_e, etc. 393 | # Must pass that info in a separate arg, darnit. 394 | def entry_changed(entry, which): 395 | global percent_e, width_e, height_e, orig_width, orig_height, busy 396 | if busy: 397 | return 398 | 399 | # Can't use get_value() or get_value_as_int() because they 400 | # don't work before an update(), but update() will usually 401 | # overwrite what the user typed. So define a function: 402 | def get_num_value(spinbox): 403 | s = spinbox.get_text() 404 | if not s: 405 | return 0 406 | 407 | try: 408 | p = int(s) 409 | return p 410 | except ValueError: 411 | try: 412 | p = float(s) 413 | return p 414 | except ValueError: 415 | return 0 416 | 417 | if which == 'p': 418 | p = get_num_value(percent_e) 419 | if not p: return 420 | busy = True 421 | w = int(orig_width * p / 100.) 422 | width_e.set_text(str(w)) 423 | h = int(orig_height * p / 100.) 424 | height_e.set_text(str(h)) 425 | busy = False 426 | 427 | elif which == 'w': 428 | w = get_num_value(width_e) 429 | if not w: return 430 | busy = True 431 | p = w * 100. / orig_width 432 | percent_e.set_text(str(p)) 433 | h = int(orig_height * p / 100.) 434 | height_e.set_text(str(h)) 435 | busy = False 436 | 437 | elif which == 'h': 438 | h = get_num_value(height_e) 439 | if not h: return 440 | busy = True 441 | p = h * 100. / orig_height 442 | percent_e.set_text(str(p)) 443 | w = int(orig_width * p / 100.) 444 | width_e.set_text(str(w)) 445 | busy = False 446 | else: 447 | print "I'm confused -- not width, height or percentage" 448 | 449 | register( 450 | "python_fu_saver", 451 | "Save or export the current image, optionally also exporting a scaled version, prompting for filename only if needed.", 452 | "Save or export the current image, optionally also exporting a scaled version, prompting for filename only if needed.", 453 | "Akkana Peck", 454 | "Akkana Peck", 455 | "2014", 456 | "Saver", 457 | "*", 458 | [ 459 | (PF_IMAGE, "image", "Input image", None), 460 | (PF_DRAWABLE, "drawable", "Input drawable", None), 461 | ], 462 | [], 463 | python_fu_saver, 464 | menu = "/File/Save/" 465 | ) 466 | 467 | register( 468 | "python_fu_saver_as", 469 | "Prompt to save or export the current image, optionally also exporting a scaled version.", 470 | "Prompt to save or export the current image, optionally also exporting a scaled version.", 471 | "Akkana Peck", 472 | "Akkana Peck", 473 | "2014", 474 | "Saver as...", 475 | "*", 476 | [ 477 | (PF_IMAGE, "image", "Input image", None), 478 | (PF_DRAWABLE, "drawable", "Input drawable", None), 479 | ], 480 | [], 481 | python_fu_saver_as, 482 | menu = "/File/Save/" 483 | ) 484 | 485 | main() 486 | 487 | -------------------------------------------------------------------------------- /gimp2/sf-helloworld.scm: -------------------------------------------------------------------------------- 1 | ; "Hello, World" Test 2 | ; 3 | ; Creates an image with the text "Hello, World!" 4 | 5 | ; Copyright 2010, Akkana Peck 6 | ; This program is free software; you can redistribute it and/or modify 7 | ; it under the terms of the GNU General Public License as published by 8 | ; the Free Software Foundation; either version 2 of the License, or 9 | ; (at your option) any later version. 10 | ; 11 | ; This program is distributed in the hope that it will be useful, 12 | ; but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ; GNU General Public License for more details. 15 | ; 16 | ; You should have received a copy of the GNU General Public License 17 | ; along with this program; if not, write to the Free Software 18 | ; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 19 | 20 | (define (script-fu-helloworld text font size colour) 21 | (gimp-context-push) 22 | (gimp-context-set-foreground colour) 23 | (let* ( 24 | ; initial image size is 10x10 -- we'll resize it later 25 | (img (car (gimp-image-new 1 1 RGB))) 26 | (dummy (gimp-image-undo-disable img)) 27 | (text-layer (car (gimp-text-fontname img -1 0 0 text 10 28 | TRUE size PIXELS font))) 29 | (width (car (gimp-drawable-width text-layer))) 30 | (height (car (gimp-drawable-height text-layer))) 31 | ) 32 | (gimp-image-resize img width height 0 0) 33 | 34 | (gimp-image-undo-enable img) 35 | (gimp-display-new img) 36 | (gimp-context-pop) 37 | )) 38 | 39 | (script-fu-register "script-fu-helloworld" 40 | "_Hello World (SF)..." 41 | "Creates an image with a user specified text string." 42 | "Akkana Peck " 43 | "Akkana Peck" 44 | "May, 2010" 45 | "" 46 | SF-STRING "Text string" "Hello, World!" 47 | SF-FONT "Font" "Sans" 48 | SF-ADJUSTMENT "Font size (pixels)" '(100 2 1000 1 10 0 1) 49 | SF-COLOR "Color" '(255 0 0) 50 | ) 51 | 52 | (script-fu-menu-register "script-fu-helloworld" 53 | "/File/Create") 54 | -------------------------------------------------------------------------------- /gimp2/stack.scm: -------------------------------------------------------------------------------- 1 | ;; stack.scm: Make an averaged image stack: 2 | ;; combine all the layers with opacity 1/num_images. 3 | ;; Copyright (C) 2007 by Akkana Peck, akkana@shallowsky.com. 4 | ;; 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License. 7 | 8 | (define (script-fu-stack-average img drawable) 9 | (let* ((layers (gimp-image-get-layers img)) 10 | (numlayers (car layers)) 11 | (layer-array (cadr layers)) 12 | (i 0) 13 | ) 14 | (gimp-message "Stacking") 15 | (gimp-image-undo-group-start img) 16 | ;(gimp-context-push) 17 | 18 | ;; Loop over the layers. 19 | ;; Layers are numbered starting with 0 as the top layer in the stack. 20 | (while (< i numlayers) 21 | (gimp-message (number->string i)) 22 | (let* ((thislayer (aref layer-array i))) 23 | (gimp-layer-set-opacity thislayer (* 100 (/ 1 (- numlayers i)))) 24 | ) 25 | (set! i (+ i 1)) 26 | ) 27 | 28 | ;(gimp-message "") 29 | ;(gimp-context-pop) 30 | (gimp-image-undo-group-end img) 31 | (gimp-displays-flush) 32 | ) ; close let 33 | ) 34 | 35 | (if (symbol-bound? 'script-fu-menu-register (the-environment)) 36 | (begin 37 | (script-fu-register "script-fu-stack-average" 38 | _"Stack (Average)..." 39 | _"Set all layers' opacity to 1/N" 40 | "Akkana Peck" 41 | "Akkana Peck" 42 | "January 2008" 43 | "*" 44 | SF-IMAGE "Image" 0 45 | SF-DRAWABLE "Drawable" 0 46 | ) 47 | 48 | (script-fu-menu-register "script-fu-stack-average" 49 | _"/Filters/Combine") 50 | ) ; end begin 51 | (script-fu-register "script-fu-stack-average" 52 | _"/Filters/Combine/Stack (Average)..." 53 | _"Line up layers as a panorama" 54 | "Akkana Peck" 55 | "Akkana Peck" 56 | "January 2008" 57 | "*" 58 | SF-IMAGE "Image" 0 59 | SF-DRAWABLE "Drawable" 0 60 | ) 61 | ) 62 | 63 | -------------------------------------------------------------------------------- /gimp2/wallpaper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # wallpaper.py v. 0.4: a script to aid in making desktop backgrounds. 4 | # Start by making a selection (use Save Options in the Tool Options 5 | # dialog to simplify choosing specific aspect ratios). 6 | # The script knows a few common sizes (1680x1050, 1024x768 and so forth) 7 | # and chooses one based on the aspect ratio of the selection. 8 | # It will make a cropped, scaled copy which you can save as you choose. 9 | # 10 | 11 | # Copyright 2009 by Akkana Peck, http://www.shallowsky.com/software/ 12 | # You may use and distribute this plug-in under the terms of the GPL v2 13 | # or, at your option, any later GPL version. 14 | 15 | from gimpfu import * 16 | import gtk 17 | import os 18 | 19 | # 20 | # Wallpaperdir: Where you keep your wallpaper images. Change this! 21 | # Wallpapers will be saved in subdirectories named by the 22 | # horizontal resolution, e.g. $HOME/Backgrounds/1024/filename.jpg 23 | # 24 | wallpaperdir = os.path.join(os.getenv("HOME"), "Images/Backgrounds") 25 | 26 | # 27 | # Table of desired resolutions. 28 | # The script will choose the size with the aspect ratio closest 29 | # to the aspect ratio of the selection. 30 | # 31 | # WARNING! many of these have the same 4x3 aspect ratio. 32 | # Therefore, only one of them will work here. Choose one. 33 | # If you want both, you'll have to write a little more code 34 | # to produce two images in that case. 35 | # It's probably best to comment out all but the resolutions 36 | # you personally are interested in. (Ideally this would be configurable 37 | # through some sort of nice gui, and saved in preferences. :-) 38 | # 39 | common_resolutions = [ 40 | [ 1920, 1200 ], 41 | [ 1920, 1080 ], 42 | # [ 1600, 1200 ], 43 | [ 1680, 1050 ], 44 | # [ 1366, 768 ], 45 | # [ 1280, 1024 ], 46 | # [ 1024, 768 ], 47 | # [ 1039, 697 ], 48 | 49 | [ 1080, 1920 ], # galaxy s5 50 | [ 1080, 2220 ], # pixel 3a 51 | ] 52 | 53 | # 54 | # End of user-specified changes. 55 | # You shouldn't have to change anything below here. 56 | # 57 | 58 | def python_wallpaper(img, layer) : 59 | gimp.context_push() 60 | 61 | (x1,y1,x2,y2) = layer.mask_bounds 62 | sel_aspect = float(x2 - x1) / (y2 - y1) 63 | 64 | # Figure out which size we're targeting 65 | diff = 100 66 | width = 1600 67 | height = 1200 68 | for res in common_resolutions : 69 | res_aspect = float(res[0]) / res[1] 70 | if (abs(res_aspect - sel_aspect) < diff) : 71 | width = res[0] 72 | height = res[1] 73 | diff = abs(res_aspect - sel_aspect) 74 | if diff > .25 : # That different, there's probably something wrong 75 | errdialog = gtk.MessageDialog(None, 0, 76 | gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, 77 | "No preset size matches aspect ratio " + \ 78 | str(sel_aspect)) 79 | errdialog.show_all() 80 | errdialog.run() 81 | return 82 | #print "Making wallpaper of size", width, "x", height 83 | 84 | # If it's an XCF, save it as a JPG 85 | # However, in gimp 2.8, img.name is "Untitled" if the image 86 | # hasn't been saved as an XCF, which this image likely hasn't. 87 | # So test for that: 88 | if img.name.find('.') < 0 : 89 | if img.filename : 90 | name = os.path.basename(img.filename) 91 | else : 92 | # If there's neither an image name or a filename -- 93 | # e.g. it was created as new, or dragged/pasted from a browser -- 94 | # make up a placeholder and hope the user notices and changes it. 95 | name = "wallpaper.jpg" 96 | else : 97 | name = img.name 98 | if name[-4:] == ".xcf" : 99 | name = name[0:-4] + ".jpg" 100 | elif name[-7:] == ".xcf.gz" : 101 | name = name[0:-7] + ".jpg" 102 | elif name[-8:] == ".xcf.bz2" : 103 | name = name[0:-8] + ".jpg" 104 | 105 | #print wallpaperdir, width, name 106 | #print img 107 | #print dir(img) 108 | #print " " 109 | dirpathname = os.path.join(wallpaperdir, 110 | "%dx%d" % (width, height)) 111 | if not os.path.exists(dirpathname) : 112 | fulldirpathname = dirpathname 113 | dirpathname = os.path.join(wallpaperdir, str(width)) 114 | if not os.path.exists(dirpathname) : 115 | errdialog = gtk.MessageDialog(None, 0, 116 | gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, 117 | "Neither %s nor %s exists" % 118 | (fulldirpathname, dirpathname)) 119 | errdialog.show_all() 120 | errdialog.run() 121 | return 122 | 123 | pathname = os.path.join(dirpathname, name) 124 | #newimg.name = name 125 | #newimg.filename = pathname 126 | #print "Trying to set name, pathname to", name, pathname 127 | 128 | if not pdb.gimp_edit_copy_visible(img) : 129 | return 130 | 131 | # pdb.gimp_edit_paste_as_new() is deprecated, 132 | # but pdb.gimp_edit_paste_as_new_image() isn't available until 2.9. 133 | # isn't available in GIMP 2.8. 134 | version = map(int, pdb.gimp_version().split('.')) 135 | if version[0] > 2 or version[0] == 2 and version[1] > 8: 136 | newimg = pdb.gimp_edit_paste_as_new_image() 137 | else: 138 | newimg = pdb.gimp_edit_paste_as_new() 139 | 140 | # Paste-as-new creates an image with transparency, 141 | # which will warn if you try to save as jpeg, so: 142 | newimg.flatten() 143 | 144 | newimg.scale(width, height) 145 | 146 | # Copy EXIF information from the old image to the new one. 147 | # This uses an API that's new in GIMP 2.9, so it won't happen 148 | # in GIMP 2.8 and earlier. 149 | if "gimp_image_get_metadata" in dir(pdb): 150 | metadata = pdb.gimp_image_get_metadata(img) 151 | pdb.gimp_image_set_metadata(newimg, metadata) 152 | else: 153 | print "Not copying EXIF metadata -- don't have the API." 154 | 155 | # Check to make sure we won't be overwriting 156 | def check_overwrite_cb(widget) : 157 | newpath = os.path.join(pathentry.get_text(), fileentry.get_text()) 158 | if os.access(newpath, os.R_OK) : 159 | msglabel.set_text(newpath + " already exists!") 160 | dialog.set_response_sensitive(gtk.RESPONSE_OK, False) 161 | else : 162 | msglabel.set_text(" ") 163 | dialog.set_response_sensitive(gtk.RESPONSE_OK, True) 164 | 165 | # want to bring up the save dialog interactively here -- 166 | # but unfortunately there's no way to call save-as interactively 167 | # from python! So give the user a chance to change the directory: 168 | # or filename: 169 | # 170 | dialog = gtk.Dialog("Save as Wallpaper", None, 0, 171 | (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 172 | "Edit", gtk.RESPONSE_NONE, 173 | gtk.STOCK_OK, gtk.RESPONSE_OK)) 174 | #dialog.connect('destroy', lambda win: gtk.main_quit()) 175 | 176 | label = gtk.Label("Wallpaper: " + str(width) + "x" + str(height)) 177 | dialog.vbox.pack_start(label, True, True, 0) 178 | 179 | table = gtk.Table(3, 2) 180 | table.set_row_spacings(10) 181 | table.set_col_spacings(10) 182 | 183 | label = gtk.Label("Directory:") 184 | table.attach(label, 0, 1, 0, 1) 185 | pathentry = gtk.Entry() 186 | pathentry.set_width_chars(55) 187 | pathentry.set_text(dirpathname) 188 | table.attach(pathentry, 1, 2, 0, 1) 189 | 190 | label = gtk.Label("File name:") 191 | table.attach(label, 0, 1, 1, 2) 192 | fileentry = gtk.Entry() 193 | fileentry.set_width_chars(10) 194 | fileentry.set_text(name) 195 | table.attach(fileentry, 1, 2, 1, 2) 196 | 197 | msglabel = gtk.Label(" ") 198 | table.attach(msglabel, 0, 2, 2, 3) 199 | 200 | dialog.vbox.pack_start(table, True, True, 0) 201 | 202 | # set_default_response only marks the button visually -- 203 | # it doesn't actually change behavior. 204 | dialog.set_default_response(gtk.RESPONSE_OK) 205 | 206 | # To make Enter really do something, use activate on the entry: 207 | def dialogRespond(entry, dialog, response) : 208 | dialog.response(response) 209 | 210 | fileentry.connect("activate", dialogRespond, dialog, gtk.RESPONSE_OK) 211 | pathentry.connect("changed", check_overwrite_cb) 212 | fileentry.connect("changed", check_overwrite_cb) 213 | check_overwrite_cb(None) 214 | 215 | dialog.show_all() 216 | fileentry.grab_focus() 217 | 218 | response = dialog.run() 219 | 220 | pathname = pathentry.get_text() 221 | newname = fileentry.get_text() 222 | pathname = os.path.join(pathname, newname) 223 | if newname != name : 224 | # Change the image name on the original -- so that if we make 225 | # backgrounds of any other sizes, the name will stay the same. 226 | pdb.gimp_image_set_filename(img, newname) 227 | name = newname 228 | # Set name and dirpath for the new image, in case user choses "Edit" 229 | pdb.gimp_image_set_filename(newimg, pathname) 230 | 231 | if response == gtk.RESPONSE_OK : 232 | dialog.hide() 233 | dialog.destroy() 234 | # Neither hide nor destroy will work unless we collect 235 | # gtk events before proceeding: 236 | while gtk.events_pending() : 237 | gtk.main_iteration() 238 | 239 | try : 240 | pdb.gimp_file_save(newimg, newimg.active_layer, pathname, pathname, 241 | run_mode=0) 242 | # If the save was successful, we don't need to show the new image, 243 | # so delete it: 244 | gimp.delete(newimg) 245 | gimp.context_pop() 246 | 247 | return 248 | except RuntimeError, e: 249 | print "Couldn't save!", str(e) 250 | # Is it worth bringing up a dialog here? 251 | 252 | elif response == gtk.RESPONSE_REJECT : 253 | # Cancel the whole operation -- don't show the image 254 | gimp.delete(newimg) 255 | gimp.context_pop() 256 | return 257 | 258 | # We didn't save (OK) or Cancel; user must have clicked Edit. 259 | # So display the image and let the user deal with it. 260 | gimp.Display(newimg) 261 | gimp.context_pop() 262 | 263 | #NewFileSelector(newimg, pathname) 264 | 265 | # GIMP python doesn't have any way to call up the Save As dialog! 266 | # pdb.gimp_file_save would do it if there were some way to call it 267 | # with run_mode = interactive ... 268 | # class OldFileSelector: 269 | # # Get the selected filename and print it to the console 270 | # def file_ok_sel(self, w): 271 | # print "%s" % self.filew.get_filename() 272 | 273 | # def destroy(self, widget): 274 | # gtk.main_quit() 275 | 276 | # def __init__(self, img, pathname): 277 | # # Create a new file selection widget 278 | # print "old file sel dialog for", img, pathname 279 | # self.filew = gtk.FileSelection("File selection") 280 | 281 | # self.filew.connect("destroy", self.destroy) 282 | # # Connect the ok_button to file_ok_sel method 283 | # self.filew.ok_button.connect("clicked", self.file_ok_sel) 284 | 285 | # # Connect the cancel_button to destroy the widget 286 | # self.filew.cancel_button.connect("clicked", 287 | # lambda w: self.filew.destroy()) 288 | 289 | # # Lets set the filename, as if this were a save dialog, 290 | # # and we are giving a default filename 291 | # self.filew.set_filename(pathname) 292 | 293 | # print "showing" 294 | # self.filew.show() 295 | 296 | # def NewFileSelector(img, pathname) : 297 | # # Create a new file selection widget 298 | # print "new file sel dialog for", img, pathname 299 | # chooser = gtk.FileChooserDialog(title=None, 300 | # action=gtk.FILE_CHOOSER_ACTION_SAVE, 301 | # buttons=(gtk.STOCK_CANCEL, 302 | # gtk.RESPONSE_CANCEL, 303 | # gtk.STOCK_OPEN, 304 | # gtk.RESPONSE_OK)) 305 | # chooser.set_current_name(os.path.basename(pathname)) 306 | # chooser.set_current_folder(os.path.dirname(pathname)) 307 | # response = chooser.run() 308 | # if response == gtk.RESPONSE_OK : 309 | # print "Would save to", chooser.get_filename() 310 | # else : 311 | # print "cancelled" 312 | 313 | register( 314 | "python_fu_wallpaper", 315 | "Crop and resize to make wallpaper", 316 | "Crop and resize the current image according to the selection, " 317 | "to make desktop wallpaper", 318 | "Akkana Peck", 319 | "Akkana Peck", 320 | "2009", 321 | "Selection to Wallpaper", 322 | "*", 323 | [ 324 | (PF_IMAGE, "image", "Input image", None), 325 | (PF_DRAWABLE, "drawable", "Input drawable", None), 326 | ], 327 | [], 328 | python_wallpaper, 329 | menu = "/Image/" 330 | ) 331 | 332 | main() 333 | 334 | -------------------------------------------------------------------------------- /gimp3/README.md: -------------------------------------------------------------------------------- 1 | These are the plug-ins I've ported to the GIMP 3 API 2 | (currently represented by GIMP 2.99 or the default GIMP git master build). 3 | 4 | This API is still changing (as I write this in December 2023) so it's 5 | possible they can get behind, though I'll try to keep them working. 6 | 7 | To use one of them, you'll need to make a subdirectory: you can't just copy 8 | the plug-in file as with GIMP 2.x. So for example: 9 | 10 | mkdir ~/.config/GIMP/2.99/plug-ins/saver 11 | cp saver.py ~/.config/GIMP/2.99/plug-ins/saver/ 12 | 13 | As always with plug-ins, Linux, Unix and Mac users must make sure the file 14 | is executable, e.g. 15 | ```chmod +x ~/.config/GIMP/2.99/plug-ins/saver/saver.py``` 16 | -------------------------------------------------------------------------------- /gimp3/arrowdesigner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Draw arrows in GIMP, using the selection as a guide for where to draw. 5 | # Updated for GIMP 3.0+ API. 6 | # 7 | # Copyright 2010,2011,2020,2022,2024 by Akkana, http://www.shallowsky.com/software/ 8 | # plus contributions from Robert Brizard. 9 | # Share and enjoythis plug-in under the terms of the GPL v2 or later. 10 | 11 | # GIMP 3 API: Still one problem left to figure out: 12 | # gimp_plug_in_destroy_proxies: ERROR: GimpImage proxy with ID 1 was refed by plug-in, it MUST NOT do that! 13 | 14 | 15 | import gi 16 | gi.require_version('Gimp', '3.0') 17 | from gi.repository import Gimp 18 | gi.require_version('GimpUi', '3.0') 19 | from gi.repository import GimpUi 20 | gi.require_version('Gegl', '0.4') 21 | from gi.repository import Gegl 22 | from gi.repository import GObject 23 | from gi.repository import GLib 24 | from gi.repository import Gio 25 | from gi.repository import Pango 26 | 27 | gi.require_version('Gtk', '3.0') 28 | from gi.repository import Gtk 29 | gi.require_version('Gdk', '3.0') 30 | from gi.repository import Gdk 31 | 32 | import math 33 | import sys, os 34 | 35 | # Direction "enums" 36 | DIREC_N, DIREC_NE, DIREC_E, DIREC_SE, DIREC_S, DIREC_SW, DIREC_W, DIREC_NW \ 37 | = range(8) 38 | 39 | # Which index corresponds to which direction name? 40 | DirectionNames = [ "N", "NE", "E", "SE", "S", "SW", "W", "NW" ] 41 | 42 | def N_(message): return message 43 | def _(message): return GLib.dgettext(None, message) 44 | 45 | 46 | class ArrowDesigner (Gimp.PlugIn): 47 | ## GimpPlugIn virtual methods ## 48 | 49 | # GIMP 3.0 requires that every plugin define the exact same function: 50 | def do_set_i18n(self, procname): 51 | return True, 'gimp30-python', None 52 | 53 | def do_query_procedures(self): 54 | return [ "python-fu-arrow-designer" ] 55 | 56 | def do_create_procedure(self, name): 57 | """Register as a GIMP plug-in""" 58 | procedure = Gimp.ImageProcedure.new(self, name, 59 | Gimp.PDBProcType.PLUGIN, 60 | self.show_arrowdesigner, None) 61 | 62 | procedure.set_image_types("*"); 63 | 64 | procedure.set_menu_label("_Arrow Designer"); 65 | procedure.set_icon_name(GimpUi.ICON_GEGL); 66 | procedure.add_menu_path('/Filters/Render/'); 67 | 68 | # Add parameters 69 | procedure.add_int_argument("angle", _("A_ngle"), 70 | _("Angle of the arrowhead"), 71 | 25, 80, 35, 72 | GObject.ParamFlags.READWRITE) 73 | procedure.add_int_argument("size", _("S_ize"), 74 | _("Arrowhead size (pixels)"), 75 | 1, 200, 30, 76 | GObject.ParamFlags.READWRITE) 77 | procedure.add_int_argument("direction", _("D_irection"), 78 | _("Direction the arrow points"), 79 | 0, 8, 0, 80 | GObject.ParamFlags.READWRITE) 81 | 82 | procedure.set_documentation( 83 | "Draw an arrow based on the selection", 84 | "Draw an arrow based on the current selection, " 85 | "updating interactively as the selection changes", 86 | name); 87 | procedure.set_attribution("Akkana Peck", "Akkana Peck", 88 | "2010,2011,2022,2023,2024"); 89 | 90 | return procedure 91 | 92 | def show_arrowdesigner(self, procedure, run_mode, image, layers, 93 | config, data): 94 | angle = config.get_property("angle") 95 | size = config.get_property("size") 96 | direction = config.get_property("direction") 97 | 98 | if run_mode == Gimp.RunMode.INTERACTIVE: 99 | GimpUi.init("arrowdesigner") 100 | r = ArrowWindow(image, config, angle, size, direction) 101 | Gtk.main() 102 | else: 103 | print("Non-interactive arrow designer, I'm not sure what to do!", 104 | file=sys.stderr) 105 | 106 | return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, 107 | GLib.Error()) 108 | 109 | 110 | def python_fu_arrow_from_selection(img, layer, arrowangle, arrowsize, 111 | x1, y1, x2, y2, cyc) : 112 | """ 113 | Draw an arrow from (x1, y1) to (x2, y2) with the head at (x2, y2). 114 | The the arrowhead is an isoceles triangle where arrowsize controls 115 | the length of the isoside; the tip angle is arrowangle. 116 | 'cyc' is the wanted integer number of gradient length in the shaft. 117 | The arrow's shaft is whatever size and brush set for the Paintbrush tool. 118 | """ 119 | 120 | # Save the current selection: 121 | savesel = img.get_selection().save(img) # a Gimp.Channel object 122 | Gimp.Selection.none(img) 123 | sys.stdout.flush() 124 | 125 | aangle = arrowangle * math.pi / 360. 126 | 127 | # 128 | # Draw the line first. 129 | # But don't go quite all the way to the end, because that 130 | # would make a rounded tip where the arrow point should be. 131 | # 132 | strokes = [ x1, y1, x2, y2 ] 133 | dy = y2 - y1 134 | dx = x2 - x1 135 | # length of arrowhead in the shaft direction 136 | l_head = arrowsize * math.cos(aangle) 137 | 138 | l_arrow = math.sqrt(dx*dx + dy*dy) 139 | # ratio is length_head/length_arrow, if >= 1 no line 140 | ratio = l_head / l_arrow * 0.5 141 | if ratio < 1.0 : 142 | # from similar triangles 143 | strokes[2] -= int(round(ratio*dx)) 144 | strokes[3] -= int(round(ratio*dy)) 145 | 146 | # compute the length of the gradient cycle wanted 147 | if cyc > 0: cycl_grad = int((l_arrow - l_head)/cyc) 148 | elif cyc == 0: cycl_grad = 0 149 | 150 | # TypeError: Gimp.paintbrush() takes exactly 5 arguments (6 given) 151 | Gimp.paintbrush(layer, 0, strokes, Gimp.StrokeMethod.LINE, 152 | cycl_grad) 153 | 154 | # 155 | # Now make the arrowhead 156 | # 157 | theta = math.atan2(y2-y1, x2-x1) 158 | points = [ x2, y2, 159 | int(x2 - arrowsize * math.cos(theta - aangle)), 160 | int(y2 - arrowsize * math.sin(theta - aangle)), 161 | int(x2 - arrowsize * math.cos(theta + aangle)), 162 | int(y2 - arrowsize * math.sin(theta + aangle)) ] 163 | 164 | # Only draw the head if the 3 points aren't collinear. 165 | # Otherwise, it can fill the whole arrow layer: 166 | # e.g. try arrow size 1, arrow angle < 30. 167 | if int(l_head) > 1 and points[2:4] != points[4:6] : 168 | # Select the arrowhead shape 169 | # XXX This doesn't work: if I pass just points, it raises 170 | # TypeError: Gimp.paintbrush() takes exactly 3 arguments (4 given) 171 | # but the signature definitely has 6 args, might be related to 172 | # https://gitlab.gnome.org/GNOME/gimp/-/issues/5312#note_947177 173 | # Trying Gimp.ValueArray errors saying that Gimp.ValueArray 174 | # expects one argument, a number, but it's not clear what 175 | # that number is supposed to be. 176 | img.select_polygon(Gimp.ChannelOps.REPLACE, points) 177 | 178 | # Fill the arrowhead 179 | layer.edit_fill(Gimp.FillType.FOREGROUND) 180 | else: 181 | print("Not filling, but tastes great") 182 | sys.stdout.flush() 183 | # Should stroke the selection 184 | 185 | # Restore the old selection 186 | img.select_item(Gimp.ChannelOps.REPLACE, savesel) 187 | 188 | savesel = None 189 | img = None 190 | 191 | 192 | def direc_to_coords(x1, y1, x2, y2, direction): 193 | if direction == DIREC_N : 194 | return x1, y2, x1, y1 195 | elif direction == DIREC_NE : 196 | return x1, y2, x2, y1 197 | elif direction == DIREC_E : 198 | return x1, y1, x2, y1 199 | elif direction == DIREC_SE : 200 | return x1, y1, x2, y2 201 | elif direction == DIREC_S : 202 | return x1, y1, x1, y2 203 | elif direction == DIREC_SW : 204 | return x2, y1, x1, y2 205 | elif direction == DIREC_W : 206 | return x2, y1, x1, y1 207 | elif direction == DIREC_NW : 208 | return x2, y2, x1, y1 209 | 210 | 211 | class ArrowWindow(Gtk.Window): 212 | def __init__ (self, img, config, angle, size, direction): 213 | self.img = img 214 | self.config = config 215 | 216 | self.x1, self.y1, self.x2, self.y2 = 0, 0, 0, 0 217 | 218 | self.arrowangle = angle 219 | self.arrowsize = size 220 | self.direction = direction 221 | 222 | self.changed = False 223 | self.num_grad = 0 224 | 225 | self.bounds = None 226 | 227 | # Make a new GIMP layer to draw on 228 | self.layer = Gimp.Layer.new(img, "arrow", 229 | img.get_width(), img.get_height(), 230 | Gimp.ImageType.RGBA_IMAGE, 100, 231 | Gimp.LayerMode.NORMAL) 232 | # XXX layer needn't be img wxh, should be the size of the selection. 233 | img.insert_layer(self.layer, None, 0) 234 | 235 | # Create the dialog 236 | win = Gtk.Window.__init__(self) 237 | self.set_title("GIMP Arrow Designer") 238 | 239 | # Mac may have a problem with the window disappearing below 240 | # the image window. But on small screens, the window is big 241 | # enough that it can block a lot of the image window. 242 | # Ideally, it would be nice to make sure it's initially 243 | # on top, but then let the user hide it later. 244 | # Or make a checkbox for it in the dialog, but that would 245 | # make the dialog even bigger. 246 | #self.set_keep_above(True) # keep the window on top 247 | 248 | # Obey the window manager quit signal: 249 | self.connect("destroy", Gtk.main_quit) 250 | 251 | self.set_border_width(10) 252 | vbox = Gtk.VBox(spacing=10, homogeneous=False) 253 | self.add(vbox) 254 | label = Gtk.Label(label="""Arrow designer by Akkana 255 | Make a rectangular selection and adjust the arrowhead size and angle. 256 | Uses the selection, Paintbrush brush foreground color and gradient.""") 257 | 258 | # Change color of the label first line R. B. 259 | # But nobody seems to know how to do this sort of thing 260 | # in the GI world. 261 | # attr = Pango.AttrList() 262 | # fg_color = Pango.AttrForeground(0, 0, 65535, 0, 30) 263 | # size = Pango.AttrSize(17000, 0, 14) 264 | # bold = Pango.AttrWeight(Pango.WEIGHT_ULTRABOLD, 0, 14) 265 | # attr.insert(fg_color) 266 | # attr.insert(size) 267 | # attr.insert(bold) 268 | # label.set_attributes(attr) 269 | 270 | vbox.add(label) 271 | label.show() 272 | 273 | table = Gtk.Table(n_rows=3, n_columns=2, homogeneous=False) 274 | # Deprecated: 275 | # table.set_col_spacings(10) 276 | vbox.add(table) 277 | 278 | # Arrow size and sharpness 279 | label = Gtk.Label(label="Arrowhead size (px)") 280 | # label.set_alignment(xalign=0.0, yalign=1.0) 281 | table.attach(label, 0, 1, 0, 1, 282 | xoptions=Gtk.AttachOptions.FILL, yoptions=0) 283 | label.show() 284 | scale = Gtk.Scale.new_with_range(min=0, max=400, step=1., 285 | orientation=Gtk.Orientation.HORIZONTAL) 286 | scale.set_digits(0) 287 | scale.set_value(self.arrowsize) 288 | scale.connect("value_changed", self.arrowsize_cb) 289 | table.attach(scale, 1, 2, 0, 1) 290 | scale.show() 291 | 292 | label = Gtk.Label(label="Arrowhead angle (°)") 293 | # label.set_alignment(xalign=0.0, yalign=1.0) 294 | table.attach(label, 0, 1, 1, 2, 295 | xoptions=Gtk.AttachOptions.FILL, yoptions=0) 296 | label.show() 297 | scale = Gtk.Scale.new_with_range(min=1., max=80, step=1., 298 | orientation=Gtk.Orientation.HORIZONTAL) 299 | scale.set_digits(0) 300 | scale.set_value(self.arrowangle) 301 | scale.connect("value_changed", self.arrowangle_cb) 302 | table.attach(scale, 1, 2, 1, 2) 303 | scale.show() 304 | 305 | label = Gtk.Label(label="Gradient repetitions") 306 | # XXX DeprecationWarning: Gtk.Misc.set_alignment is deprecated 307 | # and I haven't found a new way that works in GTK3. 308 | # label.set_alignment(xalign=0.0, yalign=1.0) 309 | table.attach(label, 0, 1, 2, 3, 310 | xoptions=Gtk.AttachOptions.FILL, yoptions=0) 311 | label.show() 312 | scale = Gtk.Scale.new_with_range(min=1., max=50, step=1., 313 | orientation=Gtk.Orientation.HORIZONTAL) 314 | scale.set_digits(0) 315 | scale.set_value(self.num_grad) 316 | scale.connect("value_changed", self.num_grad_cb) 317 | table.attach(scale, 1, 2, 2, 3) 318 | scale.show() 319 | 320 | table.show() 321 | 322 | # Selector for arrow direction 323 | hbox = Gtk.HBox(spacing=5) 324 | 325 | # GTK's weirdo radiobuttons: group=None for the first button 326 | # in a group, and for the rest, group=firstbutton. 327 | group = None 328 | for direc_index, direc_name in enumerate(DirectionNames): 329 | btn = Gtk.RadioButton(group=group, label=direc_name) 330 | if not group: 331 | group = btn 332 | if direc_index == self.direction: 333 | btn.set_active(True) 334 | btn.connect("toggled", self.direction_cb, direc_index) 335 | hbox.add(btn) 336 | btn.show() 337 | 338 | vbox.add(hbox) 339 | hbox.show() 340 | 341 | # Make the dialog buttons box 342 | hbox = Gtk.HBox(spacing=20) 343 | 344 | btn = Gtk.Button(label="Next arrow") 345 | btn.connect("pressed", self.next_arrow) 346 | hbox.add(btn) 347 | btn.show() 348 | 349 | btn = Gtk.Button(label="Close") 350 | btn.connect("clicked", self.close_window) 351 | hbox.add(btn) 352 | btn.show() 353 | 354 | vbox.add(hbox) 355 | hbox.show() 356 | vbox.show() 357 | self.show() 358 | 359 | GLib.timeout_add(300, self.update, self) 360 | 361 | return win 362 | 363 | def close_window(self, widget) : 364 | # Autocrop our new layer before closing. 365 | # autocrop_layer crops the current layer using the layer 366 | # passed in as its crop template -- not clear from the doc. 367 | self.img.active_layer = self.layer 368 | 369 | pdb = Gimp.get_pdb() 370 | pdb_proc = pdb.lookup_procedure('gimp-image-autocrop-selected-layers') 371 | self.img.set_selected_layers([self.layer]) 372 | pdb_config = pdb_proc.create_config() 373 | pdb_config.set_property('image', self.img) 374 | pdb_config.set_property('drawable', self.layer) 375 | result = pdb_proc.run(pdb_config) 376 | 377 | # Unreference image and layer references to try to avoid 378 | # gimp_plug_in_destroy_proxies: ERROR: GimpImage proxy with ID 1 was refed by plug-in, it MUST NOT do that! 379 | # and it does remove some of those lines, but there's still one left. 380 | self.img = None 381 | self.layer = None 382 | 383 | Gtk.main_quit() 384 | 385 | def direction_cb(self, btn, data=None) : 386 | # This gets called for both the old button and the new one 387 | if not btn.get_active(): 388 | return 389 | self.direction = data 390 | self.changed = True 391 | 392 | def arrowsize_cb(self, val) : 393 | self.arrowsize = val.get_value() 394 | self.changed = True 395 | 396 | def arrowangle_cb(self, val) : 397 | self.arrowangle = val.get_value() 398 | self.changed = True 399 | 400 | def num_grad_cb(self, val) : 401 | self.num_grad = val.get_value() 402 | self.changed = True 403 | 404 | def arrow(self, x1, y1, x2, y2) : 405 | python_fu_arrow_from_selection(self.img, self.layer, 406 | self.arrowangle, self.arrowsize, 407 | x1, y1, x2, y2, self.num_grad) 408 | 409 | def update(self, *args): 410 | """Called from poll, in case the selection might have changed. 411 | Eventually GIMP 3.x might add notifications for selection change. 412 | """ 413 | # print("Update") 414 | # sys.stdout.flush() 415 | # The docs don't say what the first arg from bounds is. 416 | newbounds = Gimp.Selection.bounds(self.img) 417 | dummy, exists, x1, y1, x2, y2 = newbounds 418 | if exists and (self.changed or newbounds != self.bounds): 419 | self.bounds = newbounds 420 | sys.stdout.flush() 421 | self.changed = False 422 | self.layer.fill(Gimp.FillType.TRANSPARENT) 423 | 424 | # Draw the new arrow. 425 | # Order is from, to: arrowhead goes on second X, Y pair. 426 | x1, y1, x2, y2 = direc_to_coords(x1, y1, x2, y2, self.direction) 427 | self.arrow(x1, y1, x2, y2) 428 | 429 | # Update the config, so it will be written to plug-in-settings 430 | self.config.set_property("angle", self.arrowangle) 431 | self.config.set_property("size", self.arrowsize) 432 | self.config.set_property("direction", self.direction) 433 | 434 | Gimp.displays_flush() 435 | 436 | # Whether just redrawn or not, schedule the next timeout 437 | GLib.timeout_add(500, self.update, self) 438 | 439 | def next_arrow(self, data=None): 440 | # pdb.gimp_selection_none(self.img) 441 | # Make a new GIMP layer to draw on 442 | self.layer = Gimp.Layer(self.img, "arrow", 443 | self.img.get_width(), self.img.get_height(), 444 | RGBA_IMAGE, 100, NORMAL_MODE) 445 | self.img.add_layer(self.layer, 0) 446 | Gimp.displays_flush() 447 | 448 | 449 | def arrow_from_selection(img, layer, angle, size, direction) : 450 | exists, x1, y1, x2, y2 = Gimp.selection.bounds(img) 451 | if not exists : 452 | return 453 | 454 | x1, y1, x2, y2 = direc_to_coords(x1, y1, x2, y2, direction) 455 | 456 | python_fu_arrow_from_selection(img, layer, angle, size, x1, y1, x2, y2, 0) 457 | 458 | 459 | Gimp.main(ArrowDesigner.__gtype__, sys.argv) 460 | -------------------------------------------------------------------------------- /gimp3/blobi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2022 Akkana Peck 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import gi 18 | gi.require_version('Gimp', '3.0') 19 | from gi.repository import Gimp 20 | gi.require_version('GimpUi', '3.0') 21 | from gi.repository import GimpUi 22 | from gi.repository import Gegl 23 | # from gi.repository import Babl 24 | from gi.repository import GObject 25 | from gi.repository import GLib 26 | from gi.repository import Gio 27 | import time 28 | import sys 29 | 30 | from gimphelpers import pdb_run 31 | 32 | def N_(message): return message 33 | def _(message): return GLib.dgettext(None, message) 34 | 35 | 36 | def blobipy(procedure, run_mode, image, drawables, config, data): 37 | if run_mode == Gimp.RunMode.INTERACTIVE: 38 | GimpUi.init('python-fu-blobipy') 39 | dialog = GimpUi.ProcedureDialog(procedure=procedure, config=config) 40 | dialog.fill(None) 41 | if not dialog.run(): 42 | dialog.destroy() 43 | return procedure.new_return_values(Gimp.PDBStatusType.CANCEL, 44 | GLib.Error()) 45 | else: 46 | dialog.destroy() 47 | 48 | color = config.get_property('color') 49 | blur = config.get_property('blur') 50 | 51 | Gimp.context_push() 52 | image.undo_group_start() 53 | 54 | # Alpha to selection. pdb.gimp_selection_layer_alpha(layer) 55 | # is deprecated and replaced by gimp-image-select-item. 56 | # The arguments are image (GimpImage), operation (GimpChannelOps) 57 | # and item (GimpItem). 58 | pdb_run('gimp-image-select-item', { 59 | 'image': image, 60 | 'operation': Gimp.ChannelOps.REPLACE, 61 | 'item': drawables[0] 62 | }) 63 | 64 | # Invert the selection: 65 | pdb_run('gimp-selection-invert', { 66 | 'image': image 67 | }) 68 | 69 | # Make a dropshadow from the inverted selection. 70 | # I hope these parameter labels change to real names instead of types. 71 | pdb_run('script-fu-drop-shadow', { 72 | 'run-mode': Gimp.RunMode.NONINTERACTIVE, 73 | 'image': image, 74 | 'drawables': drawables, # cast to Gimp.ObjectArray by gimphelpers 75 | 'adjustment': -3, # offset X 76 | 'adjustment-2': -3, # offset Y 77 | 'adjustment-3': blur, 78 | 'color': color, 79 | 'adjustment-4': 80, # opacity 80 | 'toggle': False, # allow resizing 81 | }) 82 | 83 | # Finish and clean up 84 | Gimp.Selection.none(image) 85 | Gimp.displays_flush() 86 | image.undo_group_end() 87 | Gimp.context_pop() 88 | 89 | return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error()) 90 | 91 | 92 | class BlobiPy(Gimp.PlugIn): 93 | ## Parameters ## 94 | __gproperties__ = { 95 | "blur": (int, 96 | _("Blur amout"), 97 | _("Blur amount"), 98 | 0, 50, 7, 99 | GObject.ParamFlags.READWRITE), 100 | } 101 | 102 | ## GimpPlugIn virtual methods ## 103 | def do_set_i18n(self, procname): 104 | return True, 'gimp30-python', None 105 | 106 | def do_query_procedures(self): 107 | return [ 'python-fu-blobipy' ] 108 | 109 | def do_create_procedure(self, name): 110 | # Workaround copied from foggify.py until GIMP can handle color args 111 | # in the regular __gproperties__ list 112 | Gegl.init(None) 113 | 114 | black = Gegl.Color.new("black") 115 | black.set_rgba(0, 0, 0, 1) 116 | 117 | procedure = Gimp.ImageProcedure.new(self, name, 118 | Gimp.PDBProcType.PLUGIN, 119 | blobipy, None) 120 | procedure.set_image_types("RGB*, GRAY*"); 121 | procedure.set_sensitivity_mask (Gimp.ProcedureSensitivityMask.DRAWABLE | 122 | Gimp.ProcedureSensitivityMask.DRAWABLES) 123 | procedure.set_documentation(_("Create a 3-D effect"), 124 | _("Create a blobby 3-D effect " 125 | "using an inverse drop-shadow."), 126 | name) 127 | procedure.set_menu_label(_("_BlobiPy...")) 128 | procedure.set_attribution("Akkana Peck", "Akkana Peck", "2009,2022,2024") 129 | procedure.add_menu_path ("/Filters/Light and Shadow") 130 | 131 | procedure.add_double_argument("blur", _("_Blur"), _("Blur"), 132 | 0.0, 50.0, 7.0, 133 | GObject.ParamFlags.READWRITE) 134 | procedure.add_color_argument ("color", 135 | _("Shado_w color"), _("Shadow color"), 136 | True, black, GObject.ParamFlags.READWRITE) 137 | return procedure 138 | 139 | 140 | Gimp.main(BlobiPy.__gtype__, sys.argv) 141 | -------------------------------------------------------------------------------- /gimp3/gimphelpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import gi 4 | gi.require_version('Gimp', '3.0') 5 | from gi.repository import Gimp 6 | 7 | from gi.repository import GObject 8 | 9 | def pdb_run(name, argdic): 10 | """Run something from the GIMP PDB. 11 | args should be a dictionary of the properties for the given PDB proc. 12 | """ 13 | pdb_proc = Gimp.get_pdb().lookup_procedure(name) 14 | pdb_config = pdb_proc.create_config() 15 | for key in argdic: 16 | if type(argdic[key]) is list: 17 | pdb_config.set_core_object_array(key, argdic[key]) 18 | else: 19 | pdb_config.set_property(key, argdic[key]) 20 | pdb_proc.run(pdb_config) 21 | 22 | -------------------------------------------------------------------------------- /gimp3/pandora.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # A port of pandora-combine.scm for GIMP 3.0 Python 4 | # Copyright 2025 by Akkana Peck, share and enjoy under the GPL v2 or later. 5 | 6 | import gi 7 | gi.require_version('Gimp', '3.0') 8 | from gi.repository import Gimp 9 | gi.require_version('GimpUi', '3.0') 10 | from gi.repository import GimpUi 11 | from gi.repository import Gegl 12 | from gi.repository import GObject 13 | from gi.repository import GLib 14 | # from gi.repository import Gio 15 | 16 | import sys 17 | 18 | 19 | def N_(message): return message 20 | def _(message): return GLib.dgettext(None, message) 21 | 22 | 23 | class Pandora (Gimp.PlugIn): 24 | ## GimpPlugIn virtual methods ## 25 | def do_set_i18n(self, procname): 26 | return True, 'gimp30-python', None 27 | 28 | def do_query_procedures(self): 29 | return [ 'python-fu-pandora-panorama' ] 30 | 31 | def do_create_procedure(self, name): 32 | procedure = Gimp.ImageProcedure.new(self, name, 33 | Gimp.PDBProcType.PLUGIN, 34 | self.pandora_combine, None) 35 | procedure.set_image_types("RGB*, GRAY*"); 36 | 37 | procedure.set_sensitivity_mask (Gimp.ProcedureSensitivityMask.DRAWABLE | 38 | Gimp.ProcedureSensitivityMask.DRAWABLES) 39 | procedure.set_documentation (_("Arrange layers as a panorama"), 40 | _("Arrange layers as a panorama"), 41 | name) 42 | procedure.set_menu_label(_("_Pandora panorama...")) 43 | procedure.set_attribution("Akkana Peck", 44 | "Akkana Peck", 45 | "2025") 46 | procedure.add_menu_path ("/Filters/Combine") 47 | 48 | procedure.add_int_argument("overlap", 49 | _("How much (percent) should layers overlap?"), 50 | _("How much (percent) should layers overlap?"), 51 | 0, 100, 50, GObject.ParamFlags.READWRITE) 52 | procedure.add_boolean_argument("right_on_top", 53 | _("Rightmost layer on top?"), 54 | _("Should the rightmost layer be on top?"), 55 | True, GObject.ParamFlags.READWRITE) 56 | procedure.add_boolean_argument("use_masks", 57 | _("Use layer masks"), 58 | _("Use layer masks to fade each layer into the next"), 59 | True, GObject.ParamFlags.READWRITE) 60 | 61 | return procedure 62 | 63 | def pandora_combine(self, procedure, run_mode, image, drawables, 64 | config, data): 65 | if run_mode == Gimp.RunMode.INTERACTIVE: 66 | GimpUi.init('python-fu-pandora-combine') 67 | 68 | dialog = GimpUi.ProcedureDialog(procedure=procedure, config=config) 69 | dialog.fill(None) 70 | if not dialog.run(): 71 | dialog.destroy() 72 | return procedure.new_return_values(Gimp.PDBStatusType.CANCEL, 73 | GLib.Error()) 74 | else: 75 | dialog.destroy() 76 | 77 | overlap = config.get_property('overlap') 78 | right_on_top = config.get_property('right_on_top') 79 | use_masks = config.get_property('use_masks') 80 | 81 | Gimp.context_push() 82 | image.undo_group_start() 83 | 84 | # Get a list of all real (non-floating) layers 85 | layers = [ l for l in image.get_layers() if not l.is_floating_sel() ] 86 | num_layers = len(layers) 87 | 88 | bottomlayer = layers[-1] 89 | 90 | # Pandora assumes that all layers are the same size as the first: 91 | # XXX change this eventually. 92 | layer_width = bottomlayer.get_width() 93 | layer_height = bottomlayer.get_height() 94 | overlap_frac = overlap / 100. 95 | extra_frac = (1 - overlap_frac) 96 | 97 | pan_img_w = layer_width * (1 + (num_layers - .3) * (1 - overlap_frac)) 98 | pan_img_h = layer_height * 1.5 99 | # print("New image will be", pan_img_w, pan_img_h) 100 | image.resize(pan_img_w, pan_img_h, 0, 0) 101 | newy = layer_height * .25 102 | 103 | black = Gegl.Color.new("black") 104 | white = Gegl.Color.new("white") 105 | 106 | for i, thislayer in reversed(list(enumerate(layers))): 107 | this_w = thislayer.get_width() 108 | if right_on_top: 109 | newx = ((num_layers - i) - 1) * this_w * extra_frac 110 | else: 111 | newx = i * this_w * extra_frac 112 | 113 | thislayer.transform_translate(newx, newy) 114 | 115 | # Don't create the mask on the last image, or if the 116 | # image already has a mask 117 | if use_masks and not thislayer.get_mask() and i < num_layers - 1: 118 | masklayer = thislayer.create_mask(0) 119 | grad_w = this_w * overlap_frac * .5 120 | if right_on_top: 121 | grad_start = grad_w 122 | grad_end = 0 123 | else: 124 | grad_start = this_w - grad_w 125 | grad_end = this_w 126 | 127 | thislayer.add_alpha() 128 | thislayer.add_mask(masklayer) 129 | Gimp.context_set_foreground(white) 130 | Gimp.context_set_background(black) 131 | 132 | masklayer.edit_gradient_fill(0, # gradient-type GRADIENT_LINEAR 133 | 0, # offset 134 | False, 0, 0, # supersample 135 | False, # dither 136 | grad_start, 0, grad_end, 0) 137 | thislayer.set_edit_mask(False) 138 | 139 | # Finish and clean up 140 | Gimp.Selection.none(image) 141 | Gimp.displays_flush() 142 | image.undo_group_end() 143 | Gimp.context_pop() 144 | 145 | return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, 146 | GLib.Error()) 147 | 148 | 149 | Gimp.main(Pandora.__gtype__, sys.argv) 150 | -------------------------------------------------------------------------------- /gimp3/wallpaper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # wallpaper.py v. 0.9: a script to aid in making desktop backgrounds. 4 | # Start by making a selection (use Save Options in the Tool Options 5 | # dialog to simplify choosing specific aspect ratios). 6 | # The script knows a few common sizes (1680x1050, 1024x768 and so forth) 7 | # and chooses one based on the aspect ratio of the selection. 8 | # It will make a cropped, scaled copy which you can save as you choose. 9 | # 10 | 11 | # Copyright 2009,2023 by Akkana Peck, http://www.shallowsky.com/software/ 12 | # You may use and distribute this plug-in under the terms of the GPL v2 13 | # or, at your option, any later GPL version. 14 | 15 | import gi 16 | gi.require_version('Gimp', '3.0') 17 | from gi.repository import Gimp 18 | gi.require_version('GimpUi', '3.0') 19 | from gi.repository import GimpUi 20 | from gi.repository import GObject 21 | from gi.repository import GLib 22 | from gi.repository import Gio 23 | 24 | gi.require_version('Gtk', '3.0') 25 | from gi.repository import Gtk 26 | gi.require_version('Gdk', '3.0') 27 | from gi.repository import Gdk 28 | 29 | import os, sys 30 | 31 | def N_(message): return message 32 | def _(message): return GLib.dgettext(None, message) 33 | 34 | ###################### User-defined variables: 35 | ## You may want to change these according to your preferences 36 | 37 | # 38 | # Wallpaperdir: Where you keep your wallpaper images. Change this if needed! 39 | # Wallpapers will be saved in subdirectories named by the 40 | # horizontal resolution, e.g. $HOME/Backgrounds/1024/filename.jpg 41 | # 42 | wallpaperdir = os.path.join(os.getenv("HOME"), "Images/Backgrounds") 43 | 44 | # 45 | # Table of desired resolutions. 46 | # The script will choose the size with the aspect ratio closest 47 | # to the aspect ratio of the selection. 48 | # 49 | # WARNING! many of these have the same aspect ratio, or close enough. 50 | # Therefore, only one of them will work here. Choose one. 51 | # If you want both, you'll have to write a little more code 52 | # to produce two images in that case. 53 | # It's probably best to comment out all but the resolutions 54 | # you personally are interested in. (Ideally this would be configurable 55 | # through some sort of nice gui, and saved in preferences.:-) 56 | # 57 | common_resolutions = [ 58 | [ 1920, 1200 ], 59 | [ 1920, 1080 ], 60 | # [ 1600, 1200 ], 61 | [ 1680, 1050 ], 62 | # [ 1366, 768 ], 63 | # [ 1280, 1024 ], 64 | # [ 1024, 768 ], 65 | # [ 1039, 697 ], 66 | 67 | # [ 1080, 1920 ], # galaxy s5 68 | # [ 1080, 2220 ], # pixel 3a 69 | [ 1080, 2400 ], # pixel 6a 70 | ] 71 | 72 | # 73 | ###################### End User-defined variables. 74 | # You shouldn't have to change anything below here. 75 | 76 | 77 | class WallpaperPlugin(Gimp.PlugIn): 78 | ## Parameters ## 79 | __gproperties__ = { 80 | } 81 | 82 | ## GimpPlugIn virtual methods ## 83 | def do_set_i18n(self, procname): 84 | return True, 'gimp30-python', None 85 | 86 | def do_query_procedures(self): 87 | return [ "python-fu-wallpaper" ] 88 | 89 | def do_create_procedure(self, name): 90 | """Register as a GIMP plug-in. 91 | This also gets called every time the plug-in is invoked. 92 | """ 93 | procedure = Gimp.ImageProcedure.new(self, name, 94 | Gimp.PDBProcType.PLUGIN, 95 | self.wallpaper_dialog, None) 96 | procedure.set_menu_label("Selection to _Wallpaper"); 97 | 98 | procedure.set_image_types("*"); 99 | 100 | # procedure.set_icon_name(GimpUi.ICON_GEGL); 101 | procedure.add_menu_path('/Image') 102 | 103 | procedure.set_documentation( 104 | "Create desktop wallpaper from selection", 105 | "Crop and resize the current image according to the selection, " 106 | "to make desktop wallpaper", 107 | name); 108 | procedure.set_attribution("Akkana Peck", "Akkana Peck", "2009,2023"); 109 | 110 | return procedure 111 | 112 | def wallpaper_dialog(self, procedure, run_mode, img, 113 | n_drawables, drawables, 114 | args, data): 115 | # Get bounds of selection. 116 | dummy, exists, x1, y1, x2, y2 = Gimp.Selection.bounds(img) 117 | sel_aspect = float(x2 - x1) / (y2 - y1) 118 | 119 | # Figure out which size we're targeting 120 | diff = 100 121 | width = 1600 122 | height = 1200 123 | for res in common_resolutions: 124 | res_aspect = float(res[0]) / res[1] 125 | if (abs(res_aspect - sel_aspect) < diff): 126 | width = res[0] 127 | height = res[1] 128 | diff = abs(res_aspect - sel_aspect) 129 | if diff > .25: # That different, there's probably something wrong 130 | errortext = "No known wallpaper size matches aspect ratio " \ 131 | + str(sel_aspect) 132 | # This works, but isn't needed since we can return a CALLING_ERROR 133 | # errdialog = Gtk.MessageDialog( 134 | # None, 0, 135 | # message_type=Gtk.MessageType.ERROR, 136 | # buttons=Gtk.ButtonsType.OK, 137 | # text=errortext) 138 | # errdialog.show_all() 139 | # errdialog.run() 140 | return procedure.new_return_values( 141 | Gimp.PDBStatusType.CALLING_ERROR, GLib.Error(errortext)) 142 | 143 | # Get the image's filename 144 | imgfilename = img.get_file() 145 | # this returns a GLocalFile, which is undocumented. 146 | # Get the basename to use as a filename 147 | if imgfilename: 148 | name = imgfilename.get_basename() 149 | else: 150 | # If no pathname -- 151 | # e.g. it was created, or dragged/pasted from a browser -- 152 | # use a placeholder and hope the user notices and changes it. 153 | # In gimp 2.x, we used the image's name, but in API 3 this 154 | # tends to be ugly, like '[PXL_20230920_181601862] (imported)' 155 | name = "wallpaper.jpg" 156 | 157 | lname = name.lower() 158 | if lname.endswith(".xcf"): 159 | name = name[0:-4] + ".jpg" 160 | elif lname.endswith(".xcf.gz"): 161 | name = name[0:-7] + ".jpg" 162 | elif lname.endswith(".xcf.bz2"): 163 | name = name[0:-8] + ".jpg" 164 | 165 | dirpathname = os.path.join(wallpaperdir, "%dx%d" % (width, height)) 166 | if not os.path.exists(dirpathname): 167 | fulldirpathname = dirpathname 168 | dirpathname = os.path.join(wallpaperdir, str(width)) 169 | if not os.path.exists(dirpathname): 170 | return procedure.new_return_values( 171 | Gimp.PDBStatusType.CALLING_ERROR, GLib.Error( 172 | "Neither %s nor %s exists" % 173 | (fulldirpathname, dirpathname))) 174 | 175 | pathname = os.path.join(dirpathname, name) 176 | 177 | pdb = Gimp.get_pdb() 178 | 179 | pdb_proc = pdb.lookup_procedure('gimp-edit-copy-visible') 180 | pdb_config = pdb_proc.create_config() 181 | # pdb_config.set_property('run-mode', Gimp.RunMode.NONINTERACTIVE) 182 | pdb_config.set_property('image', img) 183 | if not pdb_proc.run(pdb_config): 184 | return procedure.new_return_values( 185 | Gimp.PDBStatusType.CALLING_ERROR, GLib.Error( 186 | "Couldn't copy visible!")) 187 | 188 | pdb_proc = pdb.lookup_procedure('gimp-edit-paste-as-new-image') 189 | pdb_config = pdb_proc.create_config() 190 | # pdb_config.set_property('run-mode', Gimp.RunMode.NONINTERACTIVE) 191 | valarray = pdb_proc.run(pdb_config) 192 | # valarray.index(0) is return status, hopefully 1 is the new image 193 | newimg = valarray.index(1) 194 | 195 | pdb_proc = pdb.lookup_procedure('gimp-image-get-metadata') 196 | pdb_config = pdb_proc.create_config() 197 | # pdb_config.set_property('run-mode', Gimp.RunMode.NONINTERACTIVE) 198 | pdb_config.set_property('image', img) 199 | valarray = pdb_proc.run(pdb_config) 200 | if valarray.index(0) == Gimp.PDBStatusType.SUCCESS: 201 | metadata = valarray.index(1) 202 | # print("Read metadata", metadata) 203 | 204 | pdb_proc = pdb.lookup_procedure('gimp-image-set-metadata') 205 | pdb_config = pdb_proc.create_config() 206 | # pdb_config.set_property('run-mode', Gimp.RunMode.NONINTERACTIVE) 207 | pdb_config.set_property('image', newimg) 208 | pdb_config.set_property('metadata-string', metadata) 209 | pdb_proc.run(pdb_config) 210 | else: 211 | print("Couldn't get image metadata") 212 | 213 | # Paste-as-new may create an image with transparency, 214 | # which will warn if you try to save as jpeg, so: 215 | newimg.flatten() 216 | 217 | newimg.scale(width, height) 218 | 219 | # Check to make sure we won't be overwriting 220 | def check_overwrite_cb(widget): 221 | newpath = os.path.join(pathentry.get_text(), fileentry.get_text()) 222 | if os.access(newpath, os.R_OK): 223 | msglabel.set_text(newpath + " already exists!") 224 | dialog.set_response_sensitive(Gtk.ResponseType.OK, False) 225 | else: 226 | msglabel.set_text(" ") 227 | dialog.set_response_sensitive(Gtk.ResponseType.OK, True) 228 | 229 | # would have liked to bring up a GIMP save dialog interactively here -- 230 | # but unfortunately there's no way to call save-as interactively 231 | # from python! So make a custom dialog that at least gives the 232 | # user a chance to change the directory or filename: 233 | dialog = Gtk.Dialog("Save as Wallpaper", None, 0) 234 | dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 235 | # I don't remember what this Edit button was for 236 | # "Edit", Gtk.ResponseType.NONE, 237 | Gtk.STOCK_OK, Gtk.ResponseType.OK) 238 | 239 | label = Gtk.Label(label="Wallpaper: " + str(width) + "x" + str(height)) 240 | dialog.vbox.pack_start(label, expand=True, fill=True, padding=0) 241 | 242 | table = Gtk.Table(n_rows=3, n_columns=2) 243 | # set_row_spacings is deprecated 244 | # table.set_row_spacings(10) 245 | # table.set_col_spacings(10) 246 | 247 | label = Gtk.Label(label="Directory:") 248 | table.attach(label, 0, 1, 0, 1) 249 | pathentry = Gtk.Entry() 250 | pathentry.set_width_chars(55) 251 | pathentry.set_text(dirpathname) 252 | table.attach(pathentry, 1, 2, 0, 1) 253 | 254 | label = Gtk.Label(label="File name:") 255 | table.attach(label, 0, 1, 1, 2) 256 | fileentry = Gtk.Entry() 257 | fileentry.set_width_chars(10) 258 | fileentry.set_text(name) 259 | table.attach(fileentry, 1, 2, 1, 2) 260 | 261 | msglabel = Gtk.Label(label=" ") 262 | table.attach(msglabel, 0, 2, 2, 3) 263 | 264 | dialog.vbox.pack_start(table, True, True, 0) 265 | 266 | # set_default_response only marks the button visually -- 267 | # it doesn't actually change behavior. 268 | dialog.set_default_response(Gtk.ResponseType.OK) 269 | 270 | # To make Enter really do something, use activate on the entry: 271 | def dialogRespond(entry, dialog, response): 272 | dialog.response(response) 273 | 274 | fileentry.connect("activate", dialogRespond, dialog, 275 | Gtk.ResponseType.OK) 276 | pathentry.connect("changed", check_overwrite_cb) 277 | fileentry.connect("changed", check_overwrite_cb) 278 | check_overwrite_cb(None) 279 | 280 | dialog.show_all() 281 | fileentry.grab_focus() 282 | 283 | response = dialog.run() 284 | 285 | pathname = pathentry.get_text() 286 | newname = fileentry.get_text() 287 | pathname = os.path.join(pathname, newname) 288 | if newname != name: 289 | # Change the image name on the original -- so that if we make 290 | # backgrounds of any other sizes, the name will stay the same. 291 | # pdb.gimp_image_set_filename(img, newname) 292 | print("WARNING: can't set filename on original") 293 | name = newname 294 | # Set name and dirpath for the new image, in case user choses "Edit" 295 | # pdb.gimp_image_set_filename(newimg, pathname) 296 | 297 | if response == Gtk.ResponseType.OK: 298 | # Time to save the new image! 299 | 300 | dialog.hide() 301 | dialog.destroy() 302 | # Neither hide nor destroy will work unless we collect 303 | # gtk events before proceeding: 304 | while Gtk.events_pending(): 305 | Gtk.main_iteration() 306 | 307 | try: 308 | # print("GIMP images now:") 309 | # for i in Gimp.list_images(): 310 | # print(i.get_name(), i.get_width(), i.get_height()) 311 | # for l in i.list_layers(): 312 | # print(" ", l.get_width(), l.get_height()) 313 | pdb_proc = pdb.lookup_procedure('gimp-file-save') 314 | pdb_config = pdb_proc.create_config() 315 | pdb_config.set_property('run-mode', Gimp.RunMode.NONINTERACTIVE) 316 | pdb_config.set_property('image', newimg) 317 | layers = newimg.list_selected_layers() 318 | # print("\nSaving new image", newimg.get_name(), 319 | # newimg.get_width(), "x", newimg.get_height(), 320 | # "to", pathname) 321 | # for l in layers: 322 | # print(" layer:", l.get_width(), "x", l.get_height()) 323 | # print() 324 | # The image only has one layer, but this is a lot easier 325 | # than trying to find and save the active layer. 326 | pdb_config.set_property('num-drawables', len(layers)) 327 | pdb_config.set_property('drawables', 328 | Gimp.ObjectArray.new(Gimp.Drawable, 329 | layers, False)) 330 | pdb_config.set_property('file', Gio.File.new_for_path(pathname)) 331 | vals = pdb_proc.run(pdb_config) 332 | if vals.index(0) == Gimp.PDBStatusType.SUCCESS: 333 | # If the save was successful, no need to show the new img, 334 | # so delete it: 335 | # Gimp.delete(newimg) 336 | newimg.delete() 337 | # gimp.context_pop() 338 | return procedure.new_return_values( 339 | Gimp.PDBStatusType.SUCCESS, GLib.Error()) 340 | else: 341 | return procedure.new_return_values( 342 | Gimp.PDBStatusType.EXECUTION_ERROR, 343 | GLib.Error("Error saving: %s" % vals.index(0))) 344 | 345 | except RuntimeError as e: 346 | print("Couldn't save!", str(e)) 347 | # Is it worth bringing up a dialog here? 348 | 349 | # elif response == Gtk.ResponseType.CANCEL: 350 | 351 | # Cancel the whole operation -- don't show the image 352 | newimg.delete 353 | # Gimp.context_pop() 354 | return procedure.new_return_values(Gimp.PDBStatusType.CANCEL, 355 | GLib.Error()) 356 | 357 | 358 | Gimp.main(WallpaperPlugin.__gtype__, sys.argv) 359 | 360 | --------------------------------------------------------------------------------