Package metapho :: Module metapho
[hide private]

Source Code for Module metapho.metapho

  1  #!/usr/bin/env python 
  2   
  3  # Copyright 2013,2016 by Akkana Peck: share and enjoy under the GPL v2 or later. 
  4   
  5  ''' 
  6  These are the base class for metapho images and taggers. 
  7  Programs with better UI can inherit from these classes. 
  8   
  9  ''' 
 10   
 11  # Image and Tagger classes have to be defined here in order for 
 12  # other files to be able to use them as metapho.Image rather than 
 13  # metapho.Image.Image. I haven't found any way that lets me split 
 14  # the classes into separate files. Sigh! 
 15   
 16  import os 
 17  import collections    # for OrderedDict 
 18   
19 -class Image :
20 '''An image, with additional info such as rotation and tags. 21 ''' 22 23 g_image_list = [] 24
25 - def __init__(self, filename, displayed=True) :
26 '''Initialize an image filename. 27 Pass displayed=False if this image isn't to be shown 28 in the current session, only used for remembering 29 previously set tags. 30 ''' 31 self.filename = filename 32 self.tags = [] 33 34 self.displayed = displayed 35 36 # Rotation of the image relative to what it is on disk. 37 # None means we don't know yet, 0 means stay at 0. 38 # Note: use 270 for counter-clockwise rotation, not -90. 39 self.rot = None
40
41 - def __repr__(self) :
42 str = "Image %s" % self.filename 43 44 if self.rot : 45 str += " (rotation %s)" % self.rot 46 47 if self.tags : 48 str += " Tags: " + self.tags.__repr__() 49 50 str += '\n' 51 52 return str
53
54 - def delete(self) :
55 '''Delete the image file FROM DISK, and the image object 56 from the imageList. DOES NOT ASK FOR CONFIRMATION -- 57 do that (if desired) from the calling program. 58 ''' 59 print "Deleting", self.filename 60 os.unlink(self.filename) 61 Image.g_image_list.remove(self)
62 63 import shlex 64
65 -class Tagger(object) :
66 '''Manages tags for images. 67 ''' 68
69 - def __init__(self) :
70 '''tagger: an object to manage metapho image tags''' 71 72 # The category list is a list of lists: 73 # [ [ "First category", 3, 5, 11 ] ] 74 # means category 0 has the name "First category" and includes 75 # tags 3, 5 and 11 from the tag_list. 76 self.categories = collections.OrderedDict() 77 78 # The tag list is just a list of all tags we know about. 79 # A tag may be in several categories. 80 self.tag_list = [] 81 82 # Files from which we've read tags 83 self.tagfiles = [] 84 # the directory common to them, where we'll try to store tags 85 self.commondir = None 86 87 # Have any tags changed during this run? 88 # Don't update the Tags file if the user doesn't change anything. 89 self.changed = False 90 91 # What category are we currently processing? Default is Tags. 92 self.current_category = "Tags"
93
94 - def __repr__(self) :
95 '''Returns a string summarizing all known images and tags, 96 suitable for printing on stdout or pasting into a Tags file. 97 ''' 98 outstr = '' 99 for cat in self.categories : 100 outstr += '\ncategory ' + cat + '\n\n' 101 102 for tagno in self.categories[cat] : 103 tagstr = self.tag_list[tagno] 104 105 # No empty tag strings 106 if tagstr.strip() == '' : 107 continue 108 109 imgstr = '' 110 for img in Image.g_image_list : 111 if tagno in img.tags : 112 if ' ' in img.filename : 113 fname = '"' + img.filename + '"' 114 else : 115 fname = img.filename 116 imgstr += ' ' + fname 117 if imgstr : 118 outstr += "tag %s :" % tagstr + imgstr + '\n' 119 120 return outstr
121
122 - def rename_category(self, old, new) :
123 for i in range(len(self.categories)): 124 k,v = self.categories.popitem(False) 125 self.categories[new if old == k else k] = v
126
127 - def write_tag_file(self) :
128 '''Save the current set of tags to a Tags file chosen from 129 the top-level directory used in the images we've seen. 130 If there was a previous Tags file there, it will be saved 131 as Tags.bak. 132 ''' 133 if not self.changed : 134 print "No tags changed; not rewriting Tags file" 135 return 136 137 outpath = os.path.join(self.commondir, "Tags") 138 print "Saving to", outpath 139 if os.path.exists(outpath) : 140 os.rename(outpath, outpath + ".bak") 141 outfile = open(outpath, "w") 142 outfile.write(str(self)) 143 outfile.close()
144
145 - def read_tags(self, dirname) :
146 '''Read in tags from files named in the given directory, 147 and tag images in the imagelist appropriately. 148 Tags will be appended to the tag_list. 149 ''' 150 # Keep track of the dir common to all directories we use: 151 if self.commondir == None : 152 self.commondir = dirname 153 else : 154 self.commondir = os.path.commonprefix([self.commondir, dirname]) 155 # commonpre has a bug, see 156 # http://rosettacode.org/wiki/Find_common_directory_path#Python 157 # but this causes other problems: 158 # .rpartition(os.path.sep)[0] 159 160 # Might want to be recursive and use os.walk ... 161 # or maybe go the other way, search for Tags files 162 # *above* the current directory but not below. 163 # For now, only take the given directory. 164 '''Current format supported: 165 category Animals 166 tag squirrels: img_001.jpg img_030.jpg 167 tag horses: img_042.jpg 168 tag penguins: img 008.jpg 169 category Places 170 tag New Mexico: img_020.jpg img_042.jpg 171 tag Bruny Island: img 008.jpg 172 ''' 173 self.current_category = "Tags" 174 175 try : 176 pathname = os.path.join(dirname, "Tags") 177 fp = open(pathname) 178 self.tagfiles.append(pathname) 179 print "Opened", pathname 180 except IOError : 181 print "Couldn't find a file named Tags, trying Keywords" 182 try : 183 pathname = os.path.join(dirname, "Keywords") 184 fp = open(pathname) 185 self.tagfiles.append(pathname) 186 except IOError : 187 # Start us off with an empty tag list. 188 self.categories[self.current_category] = [] 189 # print "No Tags file in", dirname 190 return 191 192 self.current_category = "Tags" 193 for line in fp : 194 # The one line type that doesn't need a colon is a cat name. 195 if line.startswith('category ') : 196 newcat = line[9:].strip() 197 if newcat : 198 self.current_category = newcat 199 if self.current_category not in self.categories : 200 self.categories[self.current_category] = [] 201 else : 202 print "Parse error: couldn't read category name from", line 203 continue 204 205 # Any other legal line type must have a colon. 206 colon = line.find(':') 207 if colon < 0 : 208 continue # If there's no colon, it's not a legal tag line 209 210 # Now we know we have tagname, typename or photoname. 211 # Get the list of objects after the colon. 212 # Use shlex to handle quoted and backslashed 213 # filenames with embedded spaces. 214 try : 215 objects = shlex.split(line[colon+1:].strip()) 216 except ValueError: 217 print pathname, "Couldn't parse:", line 218 continue 219 220 if line.startswith('tagtype ') : 221 typename = line[8:colon].strip() 222 223 elif line.startswith('photo ') : 224 photoname = line[6:colon].strip() 225 226 else : 227 # Anything else is a tag. 228 # If it starts with "tag " (as it should), strip that off. 229 if line.startswith('tag ') : 230 tagstr = line[4:colon].strip() 231 else : 232 tagstr = line[:colon].strip() 233 234 # It may be several comma-separated tags. 235 tagnames = map(str.strip, tagstr.split(',')) 236 237 for tagname in tagnames : 238 self.process_tag(tagname, objects) 239 240 fp.close()
241
242 - def process_tag(self, tagname, filenames) :
243 '''After reading a tag from a tags file, add it to the global 244 tags list if it isn't there already, and add the given filenames 245 to it. 246 ''' 247 try : 248 tagindex = self.tag_list.index(tagname) 249 except : 250 tagindex = len(self.tag_list) 251 self.tag_list.append(tagname) 252 253 try : 254 self.categories[self.current_category].append(tagindex) 255 # KeyError if the key doesn't exist, AttributeError if 256 # self.categories[current_category] exists but isn't a list. 257 except KeyError : 258 self.categories[self.current_category] = [tagindex] 259 260 # Search for images matching the names in filenames 261 # XXX pathname issue here: filenames in tag files generally don't 262 # have absolute pathnames, so we're only matching basenames and 263 # there could be collisions. 264 for fil in filenames : 265 tagged = False 266 for img in Image.g_image_list : 267 if img.filename.endswith(fil) and tagindex not in img.tags : 268 img.tags.append(tagindex) 269 tagged = True 270 break 271 # Did we find an image matching fil? 272 # If not, add it as a non-displayed image. 273 if not tagged : 274 newim = Image(fil, displayed=False) 275 newim.tags.append(tagindex) 276 Image.g_image_list.append(newim)
277
278 - def add_tag(self, tag, img) :
279 '''Add a tag to the given image. 280 img is a metapho.Image. 281 tag may be a string, which can be a new string or an existing one, 282 or an integer index into the tag list. 283 Return the index (in the global tags list) of the tag just added, 284 or None if error. 285 ''' 286 self.changed = True 287 288 if type(tag) is int : 289 if tag not in img.tags : 290 img.tags.append(tag) 291 return tag 292 293 # Else it's a string. Make a new tag. 294 if tag in self.tag_list : 295 tagno = self.tag_list.index(tag) 296 if tagno not in self.categories[self.current_category] : 297 self.categories[self.current_category].append(tagno) 298 img.tags.append(tagno) 299 return tagno 300 301 self.tag_list.append(tag) 302 newindex = len(self.tag_list) - 1 303 img.tags.append(newindex) 304 self.categories[self.current_category].append(newindex) 305 return newindex
306
307 - def remove_tag(self, tag, img) :
308 self.changed = True 309 310 if type(tag) is int : 311 if tag in img.tags : 312 img.tags.remove(tag) 313 314 # Else it's a string. Remove it if it's there. 315 try : 316 self.tag_list.remove(tag) 317 except : 318 pass
319
320 - def clear_tags(self, img) :
321 img.tags = []
322
323 - def toggle_tag(self, tagno, img) :
324 '''Toggle tag number tagno for the given img.''' 325 self.changed = True 326 327 if tagno in img.tags : 328 img.tags.remove(tagno) 329 return 330 331 # It's not there yet. See if it exists in the global tag list. 332 # if tagno > len(self.tag_list) : 333 # print "Warning: adding a not yet existent tag", tagno 334 335 img.tags.append(tagno)
336
337 - def match_tag(self, pattern) :
338 '''Return a list of tags matching the pattern.''' 339 return None
340