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