PyXR

c:\projects\bitpim\src \ wallpaper.py



0001 ### BITPIM
0002 ###
0003 ### Copyright (C) 2003-2005 Roger Binns <rogerb@rogerbinns.com>
0004 ###
0005 ### This program is free software; you can redistribute it and/or modify
0006 ### it under the terms of the BitPim license as detailed in the LICENSE file.
0007 ###
0008 ### $Id: wallpaper.py 4410 2007-09-26 20:59:26Z djpham $
0009 
0010 "Deals with wallpaper and related views"
0011 
0012 # standard modules
0013 from __future__ import with_statement
0014 import os
0015 import sys
0016 import cStringIO
0017 import random
0018 import time
0019 
0020 # wx modules
0021 import wx
0022 import wx.lib.colourselect
0023 
0024 # my modules
0025 import brewcompressedimage
0026 import conversions
0027 import fileview
0028 import guihelper
0029 import common
0030 import helpids
0031 import pubsub
0032 import aggregatedisplay
0033 import fileinfo
0034 
0035 # do NOT import guiwidgets into this file else you'll cause a circular dependency
0036 
0037 ###
0038 ###  Wallpaper pane
0039 ###
0040 
0041 class DisplayItem(fileview.FileViewDisplayItem):
0042 
0043     datakey="wallpaper-index"
0044     datatype="Image"  # this is used in the tooltip
0045         
0046 thewallpapermanager=None
0047 
0048 class WallpaperView(fileview.FileView):
0049     CURRENTFILEVERSION=2
0050     ID_DELETEFILE=2
0051     ID_IGNOREFILE=3
0052     database_key='wallpaper-index'
0053     media_key='wallpapers'
0054     default_origin="images"
0055 
0056     # this is only used to prevent the pubsub module
0057     # from being GC while any instance of this class exists
0058     __publisher=pubsub.Publisher
0059 
0060     _bitmaptypemapping={
0061         # the extensions we use and corresponding wx types
0062         'bmp': wx.BITMAP_TYPE_BMP,
0063         'jpg': wx.BITMAP_TYPE_JPEG,
0064         'png': wx.BITMAP_TYPE_PNG,
0065         }
0066 
0067     media_notification_type=pubsub.wallpaper_type
0068 
0069     def __init__(self, mainwindow, parent, media_root):
0070         global thewallpapermanager
0071         thewallpapermanager=self
0072         self.mainwindow=mainwindow
0073         self.usewidth=10
0074         self.useheight=10
0075         self._dummy_image_filename=guihelper.getresourcefile('wallpaper.png')
0076         wx.FileSystem_AddHandler(BPFSHandler(self))
0077         self._data={self.database_key: {}}
0078         fileview.FileView.__init__(self, mainwindow, parent, media_root, "wallpaper-watermark")
0079         self.thedir=self.mainwindow.wallpaperpath
0080         self.wildcard="Image files|*.bmp;*.jpg;*.jpeg;*.png;*.gif;*.pnm;*.tiff;*.ico;*.bci;*.bit"\
0081                        "|Video files|*.3g2|All files|*.*"
0082 
0083 
0084 ##        self.bgmenu.Insert(1,guihelper.ID_FV_PASTE, "Paste")
0085 ##        wx.EVT_MENU(self.bgmenu, guihelper.ID_FV_PASTE, self.OnPaste)
0086 
0087         self.modified=False
0088         pubsub.subscribe(self.OnListRequest, pubsub.REQUEST_WALLPAPERS)
0089         self._raw_image=self._shift_down=False
0090 
0091 
0092     def updateprofilevariables(self, profile):
0093         self.usewidth=profile.WALLPAPER_WIDTH
0094         self.useheight=profile.WALLPAPER_HEIGHT
0095         self.maxlen=profile.MAX_WALLPAPER_BASENAME_LENGTH
0096         self.filenamechars=profile.WALLPAPER_FILENAME_CHARS
0097         self.convertextension=profile.WALLPAPER_CONVERT_FORMAT
0098         self.convertwxbitmaptype=self._bitmaptypemapping[self.convertextension.lower()]
0099         if hasattr(profile,"OVERSIZE_PERCENTAGE"):
0100             self.woversize_percentage=profile.OVERSIZE_PERCENTAGE
0101             self.hoversize_percentage=profile.OVERSIZE_PERCENTAGE
0102         else:
0103             self.woversize_percentage=120
0104             self.hoversize_percentage=120
0105         for o in profile.GetImageOrigins():
0106             self.media_root.AddMediaNode(o, self)
0107         self.excluded_origins=profile.excluded_wallpaper_origins
0108              
0109     def OnListRequest(self, msg=None):
0110         l=[self._data[self.database_key][x].name \
0111            for x in self._data[self.database_key] \
0112                if self._data[self.database_key][x].origin not in self.excluded_origins ]
0113         l.sort()
0114         pubsub.publish(pubsub.ALL_WALLPAPERS, l)
0115 
0116     def GetDeleteInfo(self):
0117         return guihelper.ART_DEL_WALLPAPER, "Delete Wallpaper"
0118 
0119     def GetAddInfo(self):
0120         return guihelper.ART_ADD_WALLPAPER, "Add Wallpaper"
0121 
0122     def OnAdd(self, evt=None):
0123         self._raw_image=self._shift_down
0124         super(WallpaperView, self).OnAdd(evt)
0125         # reset the fla
0126         self._shift_down=False
0127 
0128     def GetSections(self):
0129         # get all the items
0130         items=[DisplayItem(self, key) for key in self._data[self.database_key] if self._data[self.database_key][key].mediadata!=None]
0131 
0132         self.sections=[]
0133 
0134         if len(items)==0:
0135             return self.sections
0136         
0137         # get the current sorting type
0138         for sectionlabel, items in self.organizeby_Origin(items):
0139             self.media_root.AddMediaNode(sectionlabel, self)
0140             sh=aggregatedisplay.SectionHeader(sectionlabel)
0141             sh.itemsize=(self.usewidth+120, self.useheight+DisplayItem.PADDING*2)
0142             for item in items:
0143                 item.thumbnailsize=self.usewidth, self.useheight
0144             # sort items by name
0145             items.sort(self.CompareItems)
0146             self.sections.append( (sh, items) )
0147         return [sh for sh,items in self.sections]
0148 
0149     def GetItemSize(self, sectionnumber, sectionheader):
0150         return sectionheader.itemsize
0151 
0152     def GetItemsFromSection(self, sectionnumber, sectionheader):
0153         return self.sections[sectionnumber][1]
0154 
0155     def organizeby_Origin(self, items):
0156         types={}
0157         for item in items:
0158             t=item.origin
0159             if t is None: t="Default"
0160             l=types.get(t, [])
0161             l.append(item)
0162             types[t]=l
0163 
0164         keys=types.keys()
0165         keys.sort()
0166         return [ (key, types[key]) for key in types]
0167         
0168 
0169     def GetItemThumbnail(self, data, width, height, fileinfo=None):
0170         img=self.GetImageFromString(data, fileinfo)
0171         if width!=img.GetWidth() or height!=img.GetHeight():
0172             # scale the image. 
0173             sfactorw=float(width)/img.GetWidth()
0174             sfactorh=float(height)/img.GetHeight()
0175             sfactor=min(sfactorw,sfactorh) # preserve aspect ratio
0176             newwidth=int(img.GetWidth()*sfactor)
0177             newheight=int(img.GetHeight()*sfactor)
0178             img.Rescale(newwidth, newheight)
0179         return img.ConvertToBitmap()
0180 
0181     def GetImageFromString(self, data, fileinfo):
0182         """Gets the named image
0183 
0184         @return: (wxImage, filesize)
0185         """
0186         file,cons = self.GetImageConstructionInformationFromString(data, fileinfo)
0187         
0188         return cons(file)
0189 
0190     def GetImage(self, name, origin):
0191         """Gets the named image
0192 
0193         @return: (wxImage, filesize)
0194         """
0195         # find the image
0196         for _, _item in self._data[self.database_key].items():
0197             if (origin is None or _item.origin==origin) and \
0198                _item.name==name:
0199                     return self.GetImageFromString(_item.mediadata, None)
0200 
0201     # This function exists because of the constraints of the HTML
0202     # filesystem stuff.  The previous code was reading in the file to
0203     # a wx.Image, saving it as a PNG to disk (wx.Bitmap.SaveFile
0204     # doesn't have save to memory implemented), reading it back from
0205     # disk and supplying it to the HTML code.  Needless to say that
0206     # involves unnecessary conversions and gets slower with larger
0207     # images. We supply the info so that callers can make the minimum
0208     # number of conversions possible
0209     
0210 
0211     def GetImageConstructionInformationFromString(self, data, fi):
0212         """Gets information for constructing an Image from the data
0213 
0214         @return: (filename to use, function to call that returns wxImage)
0215         """
0216         if fi==None:
0217             try:
0218                 fi=self.GetFileInfoString(data)
0219             except:
0220                 fi=None
0221         if fi:
0222             if fi.format=='AVI':
0223                 # return the 1st frame of the AVI file
0224                 return data, conversions.convertavitobmp
0225             if fi.format=='LGBIT':
0226                 # LG phones may return a proprietary wallpaper media file, LGBIT
0227                 return data, conversions.convertlgbittobmp
0228             if fi.format=='3GPP2':
0229                 # video format, can't yet display the first frame.
0230                 return data, lambda name: None
0231             return cStringIO.StringIO(data), wx.ImageFromStream
0232         return self._dummy_image_filename, wx.Image
0233 
0234     def GetImageConstructionInformation(self, file):
0235         """Gets information for constructing an Image from the file
0236 
0237         @return: (filename to use, function to call that returns wxImage)
0238         """
0239         fi=self.GetFileInfo(file)
0240         if file.endswith(".mp4") or not os.path.isfile(file):
0241             return self._dummy_image_filename, wx.Image
0242         if fi:
0243             if fi.format=='AVI':
0244                 # return the 1st frame of the AVI file
0245                 return file, conversions.convertfileavitobmp
0246             if fi.format=='LGBIT':
0247                 # LG phones may return a proprietary wallpaper media file, LGBIT
0248                 return file, conversions.convertfilelgbittobmp
0249             if fi.format=='3GPP2':
0250                 # video format, can't yet display the first frame.
0251                 return file, lambda name: None
0252             return file, wx.Image
0253         return self._dummy_image_filename, wx.Image
0254 
0255     def GetFileInfoString(self, string):
0256         return fileinfo.identify_imagestring(string)
0257 
0258     def GetFileInfo(self, filename):
0259         return fileinfo.identify_imagefile(filename)
0260 
0261     def OnAddFiles(self, filenames):
0262         for file in filenames:
0263             file_stat=os.stat(file)
0264             mtime=file_stat.st_mtime
0265             if self._raw_image:
0266                 targetfilename=self.get_media_name_from_filename(file)
0267                 data=open(file, 'rb').read()
0268                 self.AddToIndex(targetfilename, self.active_section, data, self._data, mtime)
0269             else:
0270                 # :::TODO:: do i need to handle bci specially here?
0271                 # The proper way to handle custom image types, e.g. BCI and LGBIT,
0272                 # is to add a wx.ImageHandler for it. Unfortunately wx.Image_AddHandler
0273                 # is broken in the current wxPython, so . . .
0274                 fi=self.GetFileInfo(file)
0275                 if fi is not None and fi.format=='LGBIT':
0276                     img=conversions.convertfilelgbittobmp(file)
0277                 elif fi and fi.format=='3GPP2':
0278                     # 3g2 video file, no scaling, just add
0279                     targetfilename=self.get_media_name_from_filename(file)
0280                     data=open(file, 'rb').read()
0281                     self.AddToIndex(targetfilename, self.active_section, data, self._data, mtime)
0282                     continue
0283                 else:
0284                     img=wx.Image(file)
0285                 if not img.Ok():
0286                     guihelper.MessageDialog(self, "Failed to understand the image in '"+file+"'",
0287                                             "Image not understood", style=wx.OK|wx.ICON_ERROR)
0288                     continue
0289                 self.OnAddImage(img,file,refresh=False, timestamp=mtime)
0290         self.OnRefresh()
0291 
0292     def ReplaceContents(self, name, origin, new_file_name):
0293         """Replace the contents of 'file_name' by the contents of
0294         'new_file_name' by going through the image converter dialog
0295         """
0296         fi=self.GetFileInfo(new_file_name)
0297         if fi is not None and fi.format=='LGBIT':
0298             img=conversions.convertfilelgbittobmp(new_file_name)
0299         else:
0300             img=wx.Image(new_file_name)
0301         if not img.Ok():
0302             guihelper.MessageDialog(self, "Failed to understand the image in '"+new_file_name+"'",
0303                                     "Image not understood", style=wx.OK|wx.ICON_ERROR)
0304             return
0305         with guihelper.WXDialogWrapper(ImagePreviewDialog(self, img, self.mainwindow.phoneprofile, self.active_section),
0306                                        True) as (dlg, retcode):
0307             if retcode==wx.ID_OK:
0308                 img=dlg.GetResultImage()
0309                 imgparams=dlg.GetResultParams()
0310                 # ::TODO:: temporary hack - this should really be an imgparam
0311                 extension={'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'}[imgparams['format']]
0312 
0313                 res=getattr(self, "saveimage_"+imgparams['format'])(img, imgparams)
0314                 if not res:
0315                     guihelper.MessageDialog(self, "Failed to convert the image in '"+new_file_name+"'",
0316                                             "Image not converted", style=wx.OK|wx.ICON_ERROR)
0317                 self.AddToIndex(name, origin, res, self._data)
0318 
0319     def OnAddImage(self, img, file, refresh=True, timestamp=None):
0320         # ::TODO:: if file is None, find next basename in our directory for
0321         # clipboard99 where 99 is next unused number
0322         
0323         with guihelper.WXDialogWrapper(ImagePreviewDialog(self, img, self.mainwindow.phoneprofile, self.active_section),
0324                                        True) as (dlg, retcode):
0325             if retcode==wx.ID_OK:
0326                 img=dlg.GetResultImage()
0327                 imgparams=dlg.GetResultParams()
0328                 origin=self.active_section
0329                 # if we modified the image update the timestamp
0330                 if not dlg.skip:
0331                     timestamp=int(time.time())
0332 
0333                 # ::TODO:: temporary hack - this should really be an imgparam
0334                 extension={'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'}[imgparams['format']]
0335 
0336                 # munge name
0337                 targetfilename=self.get_media_name_from_filename(file, extension)
0338 
0339                 res=getattr(self, "saveimage_"+imgparams['format'])(img, imgparams)
0340                 if not res:
0341                     guihelper.MessageDialog(self, "Failed to convert the image in '"+file+"'",
0342                                             "Image not converted", style=wx.OK|wx.ICON_ERROR)
0343                     return
0344 
0345                 self.AddToIndex(targetfilename, origin, res, self._data, timestamp)
0346                 if refresh:
0347                     self.OnRefresh()
0348 
0349     def saveimage_BMP(self, img, imgparams):
0350         if img.ComputeHistogram(wx.ImageHistogram())<=236: # quantize only does 236 or less
0351             img.SetOptionInt(wx.IMAGE_OPTION_BMP_FORMAT, wx.BMP_8BPP)
0352         with common.usetempfile('bmp') as f:
0353             if img.SaveFile(f, wx.BITMAP_TYPE_BMP):
0354                 return file(f, 'rb').read()
0355             return False
0356 
0357     def saveimage_JPEG(self, img, imgparams):
0358         img.SetOptionInt("quality", 100)        
0359         with common.usetempfile('jpg') as f:
0360             if img.SaveFile(f, wx.BITMAP_TYPE_JPEG):
0361                 return file(f, 'rb').read()
0362             return False
0363 
0364     def saveimage_PNG(self, img, imgparams):
0365         # ::TODO:: this is where the file size constraints should be examined
0366         # and obeyed
0367         with common.usetempfile('png') as f:
0368             if img.SaveFile(f, wx.BITMAP_TYPE_PNG):
0369                 return file(f, 'rb').read()
0370             return False
0371     
0372     def versionupgrade(self, dict, version):
0373         """Upgrade old data format read from disk
0374 
0375         @param dict:  The dict that was read in
0376         @param version: version number of the data on disk
0377         """
0378 
0379         # version 0 to 1 upgrade
0380         if version==0:
0381             version=1  # they are the same
0382 
0383         # 1 to 2 etc
0384         if version==1:
0385             print "converting to version 2"
0386             version=2
0387             d={}
0388             input=dict.get(self.database_key, {})
0389             for i in input:
0390                 d[i]={'name': input[i]}
0391             dict[self.database_key]=d
0392         return dict
0393 
0394 class WallpaperPreview(wx.PyWindow):
0395 
0396     def __init__(self, parent, image=None, id=1, size=wx.DefaultSize, pos=wx.DefaultPosition, style=0):
0397         wx.PyWindow.__init__(self, parent, id=id, size=size, pos=pos, style=style|wx.FULL_REPAINT_ON_RESIZE)
0398         self.bg=wx.Brush(parent.GetBackgroundColour())
0399         self._bufbmp=None
0400         
0401         wx.EVT_ERASE_BACKGROUND(self, lambda evt: None)
0402         wx.EVT_PAINT(self, self.OnPaint)
0403 
0404         self.SetImage(image)
0405 
0406     def SetImage(self, name, origin=None):
0407         if name is None:
0408             self.theimage=None
0409         else:
0410             self.theimage=thewallpapermanager.GetImage(name, origin)
0411         self.thesizedbitmap=None
0412         self.Refresh(False)
0413 
0414     def OnPaint(self, _):
0415         sz=self.GetClientSize()
0416         if self._bufbmp is None or sz.width>self._bufbmp.GetWidth() or sz.height>self._bufbmp.GetHeight():
0417             self._bufbmp=wx.EmptyBitmap((sz.width+64)&~8, (sz.height+64)&~8)
0418         dc=wx.BufferedPaintDC(self, self._bufbmp, style=wx.BUFFER_VIRTUAL_AREA)
0419         dc.SetBackground(self.bg)
0420         dc.Clear()
0421         if self.theimage is None: return
0422         # work out what size the scaled bitmap should be to retain its aspect ratio and fit within sz
0423         sfactorw=float(sz.width)/self.theimage.GetWidth()
0424         sfactorh=float(sz.height)/self.theimage.GetHeight()
0425         sfactor=min(sfactorw,sfactorh)
0426         newwidth=int(self.theimage.GetWidth()*sfactor)
0427         newheight=int(self.theimage.GetHeight()*sfactor)
0428         if self.thesizedbitmap is None or self.thesizedbitmap.GetWidth()!=newwidth or \
0429            self.thesizedbitmap.GetHeight()!=newheight:
0430             self.thesizedbitmap=self.theimage.Scale(newwidth, newheight).ConvertToBitmap()
0431         dc.DrawBitmap(self.thesizedbitmap, sz.width/2-newwidth/2, sz.height/2-newheight/2, True)
0432         
0433     
0434 
0435 
0436 def ScaleImageIntoBitmap(img, usewidth, useheight, bgcolor=None, valign="center"):
0437     """Scales the image and returns a bitmap
0438 
0439     @param usewidth: the width of the new image
0440     @param useheight: the height of the new image
0441     @param bgcolor: the background colour as a string ("ff0000" is red etc).  If this
0442                     is none then the background is made transparent"""
0443     if bgcolor is None:
0444         bitmap=wx.EmptyBitmap(usewidth, useheight, 24) # have to use 24 bit for transparent background
0445     else:
0446         bitmap=wx.EmptyBitmap(usewidth, useheight)
0447     mdc=wx.MemoryDC()
0448     mdc.SelectObject(bitmap)
0449     # scale the source. 
0450     sfactorw=usewidth*1.0/img.GetWidth()
0451     sfactorh=useheight*1.0/img.GetHeight()
0452     sfactor=min(sfactorw,sfactorh) # preserve aspect ratio
0453     newwidth=int(img.GetWidth()*sfactor/1.0)
0454     newheight=int(img.GetHeight()*sfactor/1.0)
0455 
0456     img.Rescale(newwidth, newheight)
0457     # deal with bgcolor/transparency
0458     if bgcolor is not None:
0459         transparent=None
0460         assert len(bgcolor)==6
0461         red=int(bgcolor[0:2],16)
0462         green=int(bgcolor[2:4],16)
0463         blue=int(bgcolor[4:6],16)
0464         mdc.SetBackground(wx.TheBrushList.FindOrCreateBrush(wx.Colour(red,green,blue), wx.SOLID))
0465     else:
0466         transparent=wx.Colour(*(img.FindFirstUnusedColour()[1:]))
0467         mdc.SetBackground(wx.TheBrushList.FindOrCreateBrush(transparent, wx.SOLID))
0468     mdc.Clear()
0469     mdc.SelectObject(bitmap)
0470     # figure where to place image to centre it
0471     posx=usewidth-(usewidth+newwidth)/2
0472     if valign in ("top", "clip"):
0473         posy=0
0474     elif valign=="center":
0475         posy=useheight-(useheight+newheight)/2
0476     else:
0477         assert False, "bad valign "+valign
0478         posy=0
0479     # draw the image
0480     mdc.DrawBitmap(img.ConvertToBitmap(), posx, posy, True)
0481     # clean up
0482     mdc.SelectObject(wx.NullBitmap)
0483     # deal with transparency
0484     if transparent is not None:
0485             mask=wx.Mask(bitmap, transparent)
0486             bitmap.SetMask(mask)
0487     if valign=="clip" and newheight!=useheight:
0488         return bitmap.GetSubBitmap( (0,0,usewidth,newheight) )
0489     return bitmap
0490 
0491 ###
0492 ### Virtual filesystem where the images etc come from for the HTML stuff
0493 ###
0494 
0495 statinfo=common.statinfo
0496 
0497 class BPFSHandler(wx.FileSystemHandler):
0498 
0499     CACHELOWWATER=80
0500     CACHEHIGHWATER=100
0501 
0502     def __init__(self, wallpapermanager):
0503         wx.FileSystemHandler.__init__(self)
0504         self.wpm=wallpapermanager
0505         self.cache={}
0506 
0507     def _GetCache(self, location, statinfo):
0508         """Return the cached item, or None
0509 
0510         Note that the location value includes the filename and the parameters such as width/height
0511         """
0512         if statinfo is None:
0513             print "bad location",location
0514             return None
0515         return self.cache.get( (location, statinfo), None)
0516 
0517     def _AddCache(self, location, statinfo, value):
0518         "Add the item to the cache"
0519         # we also prune it down in size if necessary
0520         if len(self.cache)>=self.CACHEHIGHWATER:
0521             print "BPFSHandler cache flush"
0522             # random replacement - almost as good as LRU ...
0523             while len(self.cache)>self.CACHELOWWATER:
0524                 del self.cache[random.choice(self.cache.keys())]
0525         self.cache[(location, statinfo)]=value
0526 
0527     def CanOpen(self, location):
0528 
0529         # The call to self.GetProtocol causes an exception if the
0530         # location starts with a pathname!  This typically happens
0531         # when the help file is opened.  So we work around that bug
0532         # with this quick check.
0533         
0534         if location.startswith("/"):
0535             return False
0536         proto=self.GetProtocol(location)
0537         if proto=="bpimage" or proto=="bpuserimage":
0538             return True
0539         return False
0540 
0541     def OpenFile(self,filesystem,location):
0542         try:
0543             res=self._OpenFile(filesystem,location)
0544         except:
0545             res=None
0546             print "Exception in getting image file - you can't do that!"
0547             print common.formatexception()
0548         if res is not None:
0549             # we have to seek the file object back to the begining and make a new
0550             # wx.FSFile each time as wxPython doesn't do the reference counting
0551             # correctly
0552             res[0].seek(0)
0553             args=(wx.InputStream(res[0]),)+res[1:]
0554             res=wx.FSFile(*args)
0555         return res
0556 
0557     def _OpenFile(self, filesystem, location):
0558         proto=self.GetProtocol(location)
0559         r=self.GetRightLocation(location)
0560         params=r.split(';')
0561         r=params[0]
0562         params=params[1:]
0563         p={}
0564         for param in params:
0565             x=param.find('=')
0566             key=str(param[:x])
0567             value=param[x+1:]
0568             if key=='width' or key=='height':
0569                 p[key]=int(value)
0570             else:
0571                 p[key]=value
0572         if proto=="bpimage":
0573             return self.OpenBPImageFile(location, r, **p)
0574         elif proto=="bpuserimage":
0575             return self.OpenBPUserImageFile(location, r, **p)
0576         return None
0577 
0578     def OpenBPUserImageFile(self, location, name, **kwargs):
0579         res=BPFSImageFile(self, location, img=self.wpm.GetImage(name, None), **kwargs)
0580         return res
0581 
0582     def OpenBPImageFile(self, location, name, **kwargs):
0583         f=guihelper.getresourcefile(name)
0584         if not os.path.isfile(f):
0585             print f,"doesn't exist"
0586             return None
0587         si=statinfo(f)
0588         res=self._GetCache(location, si)
0589         if res is not None: return res
0590         res=BPFSImageFile(self, location, name=f, **kwargs)
0591         self._AddCache(location, si, res)
0592         return res
0593 
0594 def BPFSImageFile(fshandler, location, name=None, img=None, width=-1, height=-1, valign="center", bgcolor=None):
0595     """Handles image files
0596 
0597     If we have to do any conversion on the file then we return PNG
0598     data.  This used to be a class derived from wx.FSFile, but due to
0599     various wxPython bugs it instead returns the parameters to make a
0600     wx.FSFile since a new one has to be made every time.
0601     """
0602     # if this is a bad or non-existing image file, use the dummy one!
0603     if name is None and img is None:
0604         name=guihelper.getresourcefile('wallpaper.png')
0605     # special fast path if we aren't resizing or converting image
0606     if img is None and width<0 and height<0:
0607         mime=guihelper.getwxmimetype(name)
0608         # wxPython 2.5.3 has a new bug and fails to read bmp files returned as a stream
0609         if mime not in (None, "image/x-bmp"):
0610             return (open(name, "rb"), location, mime, "", wx.DateTime_Now())
0611 
0612     if img is None:
0613         img=wx.Image(name)
0614 
0615     if width>0 and height>0:
0616         b=ScaleImageIntoBitmap(img, width, height, bgcolor, valign)
0617     else:
0618         b=img.ConvertToBitmap()
0619 
0620     with common.usetempfile('png') as f:
0621         if not b.SaveFile(f, wx.BITMAP_TYPE_PNG):
0622             raise Exception, "Saving to png failed"
0623         data=open(f, "rb").read()
0624         return (cStringIO.StringIO(data), location, "image/png", "", wx.DateTime_Now())
0625 
0626     
0627 class ImageCropSelect(wx.ScrolledWindow):
0628 
0629     def __init__(self, parent, image, previewwindow=None, id=1, resultsize=(100,100), size=wx.DefaultSize, pos=wx.DefaultPosition, style=0):
0630         wx.ScrolledWindow.__init__(self, parent, id=id, size=size, pos=pos, style=style|wx.FULL_REPAINT_ON_RESIZE)
0631         self.previewwindow=previewwindow
0632         self.bg=wx.Brush(wx.WHITE)
0633         self.parentbg=wx.Brush(parent.GetBackgroundColour())
0634         self._bufbmp=None
0635 
0636         self.anchors=None
0637         
0638         wx.EVT_ERASE_BACKGROUND(self, lambda evt: None)
0639         wx.EVT_PAINT(self, self.OnPaint)
0640 
0641         self.image=image
0642         self.origimage=image
0643         self.setresultsize(resultsize)
0644 
0645         # cursors for outside, inside, on selection, pressing bad mouse button
0646         self.cursors=[wx.StockCursor(c) for c in (wx.CURSOR_ARROW, wx.CURSOR_HAND, wx.CURSOR_SIZING, wx.CURSOR_NO_ENTRY)]
0647         self.clickpoint=None 
0648         wx.EVT_MOTION(self, self.OnMotion)
0649         wx.EVT_LEFT_DOWN(self, self.OnLeftDown)
0650         wx.EVT_LEFT_UP(self, self.OnLeftUp)
0651 
0652     def SetPreviewWindow(self, previewwindow):
0653         self.previewwindow=previewwindow
0654 
0655     def OnPaint(self, _):
0656         sz=self.thebmp.GetWidth(), self.thebmp.GetHeight()
0657         sz2=self.GetClientSize()
0658         sz=max(sz[0],sz2[0])+32,max(sz[1],sz2[1])+32
0659         if self._bufbmp is None or self._bufbmp.GetWidth()<sz[0] or self._bufbmp.GetHeight()<sz[1]:
0660             self._bufbmp=wx.EmptyBitmap((sz[0]+64)&~8, (sz[1]+64)&~8)
0661         dc=wx.BufferedPaintDC(self, self._bufbmp, style=wx.BUFFER_VIRTUAL_AREA)
0662         if sz2[0]<sz[0] or sz2[1]<sz[1]:
0663             dc.SetBackground(self.parentbg)
0664             dc.Clear()
0665         dc.DrawBitmap(self.thebmp, 0, 0, False)
0666         # draw bounding box next
0667         l,t,r,b=self.anchors
0668         points=(l,t), (r,t), (r,b), (l,b)
0669         dc.DrawLines( points+(points[0],) )
0670         for x,y in points:
0671             dc.DrawRectangle(x-5, y-5, 10, 10)
0672 
0673     OUTSIDE=0
0674     INSIDE=1
0675     HANDLE_LT=2
0676     HANDLE_RT=3
0677     HANDLE_RB=4
0678     HANDLE_LB=5
0679             
0680     def _hittest(self, evt):
0681         l,t,r,b=self.anchors
0682         within=lambda x,y,l,t,r,b:  l<=x<=r and t<=y<=b
0683         x,y=self.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
0684         for i,(ptx,pty) in enumerate(((l,t), (r,t), (r,b), (l,b))):
0685             if within(x,y,ptx-5, pty-5, ptx+5,pty+5):
0686                 return self.HANDLE_LT+i
0687         if within(x,y,l,t,r,b):
0688             return self.INSIDE
0689         return self.OUTSIDE
0690             
0691     def OnMotion(self, evt):
0692         if evt.Dragging():
0693             return self.OnMotionDragging(evt)
0694         self.UpdateCursor(evt)
0695 
0696     def UpdateCursor(self, evt):
0697         ht=self._hittest(evt)
0698         self.SetCursor(self.cursors[min(2,ht)])
0699 
0700     def OnMotionDragging(self, evt):
0701         if not evt.LeftIsDown() or self.clickpoint is None:
0702             self.SetCursor(self.cursors[3])
0703             return
0704         xx,yy=self.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
0705         deltax=xx-self.origevtpos[0]
0706         deltay=yy-self.origevtpos[1]
0707 
0708         if self.clickpoint==self.INSIDE:
0709             newanchors=self.origanchors[0]+deltax, self.origanchors[1]+deltay, \
0710                         self.origanchors[2]+deltax, self.origanchors[3]+deltay
0711             iw=self.dimensions[0]
0712             ih=self.dimensions[1]
0713             # would box be out of bounds?
0714             if newanchors[0]<0:
0715                 newanchors=0,newanchors[1], self.origanchors[2]-self.origanchors[0], newanchors[3]
0716             if newanchors[1]<0:
0717                 newanchors=newanchors[0], 0, newanchors[2], self.origanchors[3]-self.origanchors[1]
0718             if newanchors[2]>iw:
0719                 newanchors=iw-(self.origanchors[2]-self.origanchors[0]),newanchors[1],iw, newanchors[3]
0720             if newanchors[3]>ih:
0721                 newanchors=newanchors[0],ih-(self.origanchors[3]-self.origanchors[1]), newanchors[2],ih
0722             self.anchors=newanchors
0723             self.Refresh(False)
0724             self.updatepreview()
0725             return
0726         # work out how to do this with left top and then expand code
0727         if self.clickpoint==self.HANDLE_LT:
0728             aa=0,1,-1,-1
0729         elif self.clickpoint==self.HANDLE_RT:
0730             aa=2,1,+1,-1
0731         elif self.clickpoint==self.HANDLE_RB:
0732             aa=2,3,+1,+1
0733         elif self.clickpoint==self.HANDLE_LB:
0734             aa=0,3,-1,+1
0735         else:
0736             assert False, "can't get here"
0737             
0738         na=[self.origanchors[0],self.origanchors[1],self.origanchors[2],self.origanchors[3]]
0739         na[aa[0]]=na[aa[0]]+deltax
0740         na[aa[1]]=na[aa[1]]+deltay
0741         neww=na[2]-na[0]
0742         newh=na[3]-na[1]
0743         ar=float(neww)/newh
0744         if ar<self.aspectratio:
0745             na[aa[0]]=na[aa[0]]+(self.aspectratio*newh-neww)*aa[2]
0746         elif ar>self.aspectratio:
0747             na[aa[1]]=na[aa[1]]+(neww/self.aspectratio-newh)*aa[3]
0748             
0749         # ignore if image would be smaller than 10 pixels in any direction
0750         if neww<10 or newh<10:
0751             return
0752         # if any point is off screen, we need to fix things up
0753         if na[0]<0:
0754             xdiff=-na[0]
0755             ydiff=xdiff/self.aspectratio
0756             na[0]=0
0757             na[1]+=ydiff
0758         if na[1]<0:
0759             ydiff=-na[1]
0760             xdiff=ydiff*self.aspectratio
0761             na[1]=0
0762             na[0]-=xdiff
0763         if na[2]>self.dimensions[0]:
0764             xdiff=na[2]-self.dimensions[0]
0765             ydiff=xdiff/self.aspectratio
0766             na[2]=na[2]-xdiff
0767             na[3]=na[3]-ydiff
0768         if na[3]>self.dimensions[1]:
0769             ydiff=na[3]-self.dimensions[1]
0770             xdiff=ydiff*self.aspectratio
0771             na[2]=na[2]-xdiff
0772             na[3]=na[3]-ydiff
0773         if na[0]<0 or na[1]<0 or na[2]>self.dimensions[0] or na[3]>self.dimensions[1]:
0774             print "offscreen fixup not written yet"
0775             return
0776 
0777         # work out aspect ratio
0778         self.anchors=na
0779         self.Refresh(False)
0780         self.updatepreview()
0781         return
0782             
0783         
0784     def OnLeftDown(self, evt):
0785         ht=self._hittest(evt)
0786         if ht==self.OUTSIDE:
0787             self.SetCursor(self.cursors[3])
0788             return
0789         self.clickpoint=ht
0790         xx,yy=self.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
0791         self.origevtpos=xx,yy
0792         self.origanchors=self.anchors
0793         
0794     def OnLeftUp(self, evt):
0795         self.clickpoint=None
0796         self.UpdateCursor(evt)
0797 
0798     def setlbcolour(self, colour):
0799         self.bg=wx.Brush(colour)
0800         self.remakebitmap()
0801 
0802     def SetZoom(self, factor):
0803         curzoom=float(self.image.GetWidth())/self.origimage.GetWidth()
0804         self.anchors=[a*factor/curzoom for a in self.anchors]
0805         self.image=self.origimage.Scale(self.origimage.GetWidth()*factor, self.origimage.GetHeight()*factor)
0806         self.setresultsize(self.resultsize)
0807 
0808     def setresultsize(self, (w,h)):
0809         self.resultsize=w,h
0810         self.aspectratio=ratio=float(w)/h
0811         imgratio=float(self.image.GetWidth())/self.image.GetHeight()
0812         
0813         neww=self.image.GetWidth()
0814         newh=self.image.GetHeight()
0815         if imgratio<ratio:
0816             neww*=ratio/imgratio
0817         elif imgratio>ratio:
0818             newh*=imgratio/ratio
0819             
0820         # ensure a minimum size
0821         neww=max(neww, 50)
0822         newh=max(newh, 50)
0823         
0824         # update anchors if never set
0825         if self.anchors==None:
0826             self.anchors=0.1 * neww, 0.1 * newh, 0.9 * neww, 0.9 * newh
0827 
0828         # fixup anchors
0829         l,t,r,b=self.anchors
0830         l=min(neww-40, l)
0831         r=min(neww-10, r)
0832         if r-l<20: r=40
0833         t=min(newh-40, t)
0834         b=min(newh-10, b)
0835         if b-t<20: b=40
0836         aratio=float(r-l)/(b-t)
0837         if aratio<ratio:
0838             b=t+(r-l)/ratio
0839         elif aratio>ratio:
0840             r=l+(b-t)*ratio
0841         self.anchors=l,t,r,b
0842 
0843         self.dimensions=neww,newh
0844         self.thebmp=wx.EmptyBitmap(neww, newh)
0845 
0846         self.remakebitmap()
0847 
0848 
0849     def remakebitmap(self):
0850         w,h=self.dimensions
0851         dc=wx.MemoryDC()
0852         dc.SelectObject(self.thebmp)
0853         dc.SetBackground(self.bg)
0854         dc.Clear()
0855         dc.DrawBitmap(self.image.ConvertToBitmap(), w/2-self.image.GetWidth()/2, h/2-self.image.GetHeight()/2, True)
0856         dc.SelectObject(wx.NullBitmap)
0857         self.imageofthebmp=None
0858         self.SetVirtualSize( (w, h) )
0859         self.SetScrollRate(1,1)
0860 
0861         self.updatepreview()
0862         self.Refresh(False)
0863 
0864     # updating the preview is expensive so it is done on demand.  We
0865     # tell the preview window there has been an update and it calls
0866     # back from its paint method
0867     def updatepreview(self):
0868         if self.previewwindow:
0869             self.previewwindow.SetUpdated(self.GetPreview)
0870 
0871     def GetPreview(self):
0872         w,h=self.resultsize
0873         l,t,r,b=self.anchors
0874         sub=self.thebmp.GetSubBitmap( (l,t,(r-l),(b-t)) )
0875         sub=sub.ConvertToImage()
0876         sub.Rescale(w,h)
0877         return sub.ConvertToBitmap()
0878 
0879 class ImagePreview(wx.PyWindow):
0880 
0881     def __init__(self, parent):
0882         wx.PyWindow.__init__(self, parent)
0883         wx.EVT_ERASE_BACKGROUND(self, lambda evt: None)
0884         wx.EVT_PAINT(self, self.OnPaint)
0885         self.bmp=wx.EmptyBitmap(1,1)
0886         self.updater=None
0887 
0888     def SetUpdated(self, updater):
0889         self.updater=updater
0890         self.Refresh(True)
0891 
0892     def OnPaint(self, _):
0893         if self.updater is not None:
0894             self.bmp=self.updater()
0895             self.updater=None
0896         dc=wx.PaintDC(self)
0897         dc.DrawBitmap(self.bmp, 0, 0, False)
0898 
0899 
0900 class ImagePreviewDialog(wx.Dialog):
0901 
0902     SCALES=[ (0.25, "1/4"),
0903              (0.5,  "1/2"),
0904              (1, "1"),
0905              (2, "2"),
0906              (4, "4")]
0907 
0908     def __init__(self, parent, image, phoneprofile, origin):
0909         wx.Dialog.__init__(self, parent, -1, "Image Preview", style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.SYSTEM_MENU|wx.MAXIMIZE_BOX|wx.MINIMIZE_BOX)
0910         self.phoneprofile=phoneprofile
0911         self.image=image
0912         self.skip=False
0913         self.origin=origin
0914         self.target=''
0915 
0916         self.vbsouter=wx.BoxSizer(wx.VERTICAL)
0917         self.SetupControls(self.vbsouter)
0918         self.OnTargetSelect(0)
0919 
0920         wx.EVT_BUTTON(self, wx.ID_HELP, lambda _:
0921                       wx.GetApp().displayhelpid(helpids.ID_DLG_IMAGEPREVIEW))
0922 
0923         self.SetSizer(self.vbsouter)
0924         self.vbsouter.Fit(self)
0925 
0926         import guiwidgets
0927         guiwidgets.set_size("wallpaperpreview", self, 80, 1.0)
0928 
0929     def SetupControls(self, vbsouter):
0930         vbsouter.Clear(True)
0931         hbs=wx.BoxSizer(wx.HORIZONTAL)
0932         self.cropselect=ImageCropSelect(self, self.image)
0933 
0934         self.vbs=wx.BoxSizer(wx.VERTICAL)
0935         self.colourselect=wx.lib.colourselect.ColourSelect(self, wx.NewId(), "Background Color ...", (255,255,255))
0936         self.vbs.Add(self.colourselect, 0, wx.ALL|wx.EXPAND, 5)
0937         wx.lib.colourselect.EVT_COLOURSELECT(self, self.colourselect.GetId(), self.OnBackgroundColour)
0938         self.vbs.Add(wx.StaticText(self, -1, "Target"), 0, wx.ALL, 5)
0939         self.targetbox=wx.ListBox(self, -1, size=(-1,100))
0940         self.vbs.Add(self.targetbox, 0, wx.EXPAND|wx.ALL, 5)
0941         self.vbs.Add(wx.StaticText(self, -1, "Scale"), 0, wx.ALL, 5)
0942 
0943         for one,(s,_) in enumerate(self.SCALES):
0944             if s==1: break
0945         self.slider=wx.Slider(self, -1, one, 0, len(self.SCALES)-1, style=wx.HORIZONTAL|wx.SL_AUTOTICKS)
0946         self.vbs.Add(self.slider, 0, wx.ALL|wx.EXPAND, 5)
0947         self.zoomlabel=wx.StaticText(self, -1, self.SCALES[one][1])
0948         self.vbs.Add(self.zoomlabel, 0, wx.ALL|wx.ALIGN_CENTRE_HORIZONTAL, 5)
0949         
0950         self.vbs.Add(wx.StaticText(self, -1, "Preview"), 0, wx.ALL, 5)
0951         self.imagepreview=ImagePreview(self)
0952         self.cropselect.SetPreviewWindow(self.imagepreview)
0953         self.vbs.Add(self.imagepreview, 0, wx.ALL, 5)
0954 
0955         hbs.Add(self.vbs, 0, wx.ALL, 5)
0956         hbs.Add(self.cropselect, 1, wx.ALL|wx.EXPAND, 5)
0957 
0958         vbsouter.Add(hbs, 1, wx.EXPAND|wx.ALL, 5)
0959         
0960         vbsouter.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)
0961         vbsouter.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
0962 
0963         wx.EVT_SCROLL(self, self.SetZoom)
0964         wx.EVT_LISTBOX(self, self.targetbox.GetId(), self.OnTargetSelect)
0965         wx.EVT_LISTBOX_DCLICK(self, self.targetbox.GetId(), self.OnTargetSelect)
0966 
0967         self.OnOriginSelect()
0968 
0969     def ShowModal(self):
0970         # see if the image size is already correct
0971         i_w, i_h=self.image.GetSize()
0972         for v in self.targets:
0973             w,h=self.targets[v]['width'],self.targets[v]['height']
0974             if abs(i_w-w) < 5 and abs(i_h-h) < 5:
0975                 self.skip=True
0976                 return wx.ID_OK
0977         res=wx.Dialog.ShowModal(self)
0978         import guiwidgets
0979         guiwidgets.save_size("wallpaperpreview", self.GetRect())
0980         return res
0981 
0982     def SetZoom(self, evt):
0983         self.cropselect.SetZoom(self.SCALES[evt.GetPosition()][0])
0984         self.zoomlabel.SetLabel(self.SCALES[evt.GetPosition()][1])
0985         return
0986 
0987     def OnBackgroundColour(self, evt):
0988         self.cropselect.setlbcolour(evt.GetValue())
0989 
0990     def OnOriginSelect(self):
0991         self.targets=self.phoneprofile.GetTargetsForImageOrigin(self.origin)
0992         keys=self.targets.keys()
0993         keys.sort()
0994         self.targetbox.Set(keys)
0995         if self.target in keys:
0996             self.targetbox.SetSelection(keys.index(self.target))
0997         else:
0998             self.targetbox.SetSelection(0)
0999 
1000     def OnTargetSelect(self, _):
1001         self.target=self.targetbox.GetStringSelection()
1002         if self.targets.has_key(self.target):
1003             w,h=self.targets[self.target]['width'],self.targets[self.target]['height']
1004             self.imagepreview.SetSize( (w,h) )
1005             self.cropselect.setresultsize( (w, h) )
1006             self.Refresh(True)
1007         
1008     def GetResultImage(self):
1009         if self.skip:
1010             return self.image
1011         return self.imagepreview.bmp.ConvertToImage()
1012 
1013     def GetResultParams(self):
1014         return self.targets[self.targetbox.GetStringSelection()]
1015 
1016 
1017 if __name__=='__main__':
1018 
1019     if __debug__:
1020         def profile(filename, command):
1021             import hotshot, hotshot.stats, os
1022             file=os.path.abspath(filename)
1023             profile=hotshot.Profile(file)
1024             profile.run(command)
1025             profile.close()
1026             del profile
1027             howmany=100
1028             stats=hotshot.stats.load(file)
1029             stats.strip_dirs()
1030             stats.sort_stats('time', 'calls')
1031             stats.print_stats(100)
1032             stats.sort_stats('cum', 'calls')
1033             stats.print_stats(100)
1034             stats.sort_stats('calls', 'time')
1035             stats.print_stats(100)
1036             sys.exit(0)
1037 
1038     class FakeProfile:
1039 
1040         def GetImageOrigins(self):
1041             return {"images": {'description': 'General images'},
1042                     "mms": {'description': 'Multimedia Messages'},
1043                     "camera": {'description': 'Camera images'}}
1044 
1045         def GetTargetsForImageOrigin(self, origin):
1046             return {"wallpaper": {'width': 100, 'height': 200, 'description': 'Display as wallpaper'},
1047                     "photoid": {'width': 100, 'height': 150, 'description': 'Display as photo id'},
1048                     "outsidelcd": {'width': 90, 'height': 80, 'description': 'Display on outside screen'}}
1049 
1050     def run():
1051         app=wx.PySimpleApp()
1052         dlg=ImagePreviewDialog(None, wx.Image("test.jpg"), FakeProfile(), "wallpaper")
1053         dlg.ShowModal()
1054 
1055     if __debug__ and True:
1056         profile("wp.prof", "run()")
1057     run()
1058     
1059         
1060         
1061 

Generated by PyXR 0.9.4