metapho.metapho

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