Module wallpaper
[hide private]
[frames] | no frames]

Source Code for Module wallpaper

   1  ### BITPIM 
   2  ### 
   3  ### Copyright (C) 2003-2005 Roger Binns <rogerb@rogerbinns.com> 
   4  ### 
   5  ### This program is free software; you can redistribute it and/or modify 
   6  ### it under the terms of the BitPim license as detailed in the LICENSE file. 
   7  ### 
   8  ### $Id: wallpaper.py 4410 2007-09-26 20:59:26Z djpham $ 
   9   
  10  "Deals with wallpaper and related views" 
  11   
  12  # standard modules 
  13  from __future__ import with_statement 
  14  import os 
  15  import sys 
  16  import cStringIO 
  17  import random 
  18  import time 
  19   
  20  # wx modules 
  21  import wx 
  22  import wx.lib.colourselect 
  23   
  24  # my modules 
  25  import brewcompressedimage 
  26  import conversions 
  27  import fileview 
  28  import guihelper 
  29  import common 
  30  import helpids 
  31  import pubsub 
  32  import aggregatedisplay 
  33  import fileinfo 
  34   
  35  # do NOT import guiwidgets into this file else you'll cause a circular dependency 
  36   
  37  ### 
  38  ###  Wallpaper pane 
  39  ### 
  40   
41 -class DisplayItem(fileview.FileViewDisplayItem):
42 43 datakey="wallpaper-index" 44 datatype="Image" # this is used in the tooltip
45 46 thewallpapermanager=None 47
48 -class WallpaperView(fileview.FileView):
49 CURRENTFILEVERSION=2 50 ID_DELETEFILE=2 51 ID_IGNOREFILE=3 52 database_key='wallpaper-index' 53 media_key='wallpapers' 54 default_origin="images" 55 56 # this is only used to prevent the pubsub module 57 # from being GC while any instance of this class exists 58 __publisher=pubsub.Publisher 59 60 _bitmaptypemapping={ 61 # the extensions we use and corresponding wx types 62 'bmp': wx.BITMAP_TYPE_BMP, 63 'jpg': wx.BITMAP_TYPE_JPEG, 64 'png': wx.BITMAP_TYPE_PNG, 65 } 66 67 media_notification_type=pubsub.wallpaper_type 68
69 - def __init__(self, mainwindow, parent, media_root):
70 global thewallpapermanager 71 thewallpapermanager=self 72 self.mainwindow=mainwindow 73 self.usewidth=10 74 self.useheight=10 75 self._dummy_image_filename=guihelper.getresourcefile('wallpaper.png') 76 wx.FileSystem_AddHandler(BPFSHandler(self)) 77 self._data={self.database_key: {}} 78 fileview.FileView.__init__(self, mainwindow, parent, media_root, "wallpaper-watermark") 79 self.thedir=self.mainwindow.wallpaperpath 80 self.wildcard="Image files|*.bmp;*.jpg;*.jpeg;*.png;*.gif;*.pnm;*.tiff;*.ico;*.bci;*.bit"\ 81 "|Video files|*.3g2|All files|*.*" 82 83 84 ## self.bgmenu.Insert(1,guihelper.ID_FV_PASTE, "Paste") 85 ## wx.EVT_MENU(self.bgmenu, guihelper.ID_FV_PASTE, self.OnPaste) 86 87 self.modified=False 88 pubsub.subscribe(self.OnListRequest, pubsub.REQUEST_WALLPAPERS) 89 self._raw_image=self._shift_down=False
90 91
92 - def updateprofilevariables(self, profile):
93 self.usewidth=profile.WALLPAPER_WIDTH 94 self.useheight=profile.WALLPAPER_HEIGHT 95 self.maxlen=profile.MAX_WALLPAPER_BASENAME_LENGTH 96 self.filenamechars=profile.WALLPAPER_FILENAME_CHARS 97 self.convertextension=profile.WALLPAPER_CONVERT_FORMAT 98 self.convertwxbitmaptype=self._bitmaptypemapping[self.convertextension.lower()] 99 if hasattr(profile,"OVERSIZE_PERCENTAGE"): 100 self.woversize_percentage=profile.OVERSIZE_PERCENTAGE 101 self.hoversize_percentage=profile.OVERSIZE_PERCENTAGE 102 else: 103 self.woversize_percentage=120 104 self.hoversize_percentage=120 105 for o in profile.GetImageOrigins(): 106 self.media_root.AddMediaNode(o, self) 107 self.excluded_origins=profile.excluded_wallpaper_origins
108
109 - def OnListRequest(self, msg=None):
110 l=[self._data[self.database_key][x].name \ 111 for x in self._data[self.database_key] \ 112 if self._data[self.database_key][x].origin not in self.excluded_origins ] 113 l.sort() 114 pubsub.publish(pubsub.ALL_WALLPAPERS, l)
115
116 - def GetDeleteInfo(self):
117 return guihelper.ART_DEL_WALLPAPER, "Delete Wallpaper"
118
119 - def GetAddInfo(self):
120 return guihelper.ART_ADD_WALLPAPER, "Add Wallpaper"
121
122 - def OnAdd(self, evt=None):
123 self._raw_image=self._shift_down 124 super(WallpaperView, self).OnAdd(evt) 125 # reset the fla 126 self._shift_down=False
127
128 - def GetSections(self):
129 # get all the items 130 items=[DisplayItem(self, key) for key in self._data[self.database_key] if self._data[self.database_key][key].mediadata!=None] 131 132 self.sections=[] 133 134 if len(items)==0: 135 return self.sections 136 137 # get the current sorting type 138 for sectionlabel, items in self.organizeby_Origin(items): 139 self.media_root.AddMediaNode(sectionlabel, self) 140 sh=aggregatedisplay.SectionHeader(sectionlabel) 141 sh.itemsize=(self.usewidth+120, self.useheight+DisplayItem.PADDING*2) 142 for item in items: 143 item.thumbnailsize=self.usewidth, self.useheight 144 # sort items by name 145 items.sort(self.CompareItems) 146 self.sections.append( (sh, items) ) 147 return [sh for sh,items in self.sections]
148
149 - def GetItemSize(self, sectionnumber, sectionheader):
150 return sectionheader.itemsize
151
152 - def GetItemsFromSection(self, sectionnumber, sectionheader):
153 return self.sections[sectionnumber][1]
154
155 - def organizeby_Origin(self, items):
156 types={} 157 for item in items: 158 t=item.origin 159 if t is None: t="Default" 160 l=types.get(t, []) 161 l.append(item) 162 types[t]=l 163 164 keys=types.keys() 165 keys.sort() 166 return [ (key, types[key]) for key in types]
167 168
169 - def GetItemThumbnail(self, data, width, height, fileinfo=None):
170 img=self.GetImageFromString(data, fileinfo) 171 if width!=img.GetWidth() or height!=img.GetHeight(): 172 # scale the image. 173 sfactorw=float(width)/img.GetWidth() 174 sfactorh=float(height)/img.GetHeight() 175 sfactor=min(sfactorw,sfactorh) # preserve aspect ratio 176 newwidth=int(img.GetWidth()*sfactor) 177 newheight=int(img.GetHeight()*sfactor) 178 img.Rescale(newwidth, newheight) 179 return img.ConvertToBitmap()
180
181 - def GetImageFromString(self, data, fileinfo):
182 """Gets the named image 183 184 @return: (wxImage, filesize) 185 """ 186 file,cons = self.GetImageConstructionInformationFromString(data, fileinfo) 187 188 return cons(file)
189
190 - def GetImage(self, name, origin):
191 """Gets the named image 192 193 @return: (wxImage, filesize) 194 """ 195 # find the image 196 for _, _item in self._data[self.database_key].items(): 197 if (origin is None or _item.origin==origin) and \ 198 _item.name==name: 199 return self.GetImageFromString(_item.mediadata, None)
200 201 # This function exists because of the constraints of the HTML 202 # filesystem stuff. The previous code was reading in the file to 203 # a wx.Image, saving it as a PNG to disk (wx.Bitmap.SaveFile 204 # doesn't have save to memory implemented), reading it back from 205 # disk and supplying it to the HTML code. Needless to say that 206 # involves unnecessary conversions and gets slower with larger 207 # images. We supply the info so that callers can make the minimum 208 # number of conversions possible 209 210
211 - def GetImageConstructionInformationFromString(self, data, fi):
212 """Gets information for constructing an Image from the data 213 214 @return: (filename to use, function to call that returns wxImage) 215 """ 216 if fi==None: 217 try: 218 fi=self.GetFileInfoString(data) 219 except: 220 fi=None 221 if fi: 222 if fi.format=='AVI': 223 # return the 1st frame of the AVI file 224 return data, conversions.convertavitobmp 225 if fi.format=='LGBIT': 226 # LG phones may return a proprietary wallpaper media file, LGBIT 227 return data, conversions.convertlgbittobmp 228 if fi.format=='3GPP2': 229 # video format, can't yet display the first frame. 230 return data, lambda name: None 231 return cStringIO.StringIO(data), wx.ImageFromStream 232 return self._dummy_image_filename, wx.Image
233
234 - def GetImageConstructionInformation(self, file):
235 """Gets information for constructing an Image from the file 236 237 @return: (filename to use, function to call that returns wxImage) 238 """ 239 fi=self.GetFileInfo(file) 240 if file.endswith(".mp4") or not os.path.isfile(file): 241 return self._dummy_image_filename, wx.Image 242 if fi: 243 if fi.format=='AVI': 244 # return the 1st frame of the AVI file 245 return file, conversions.convertfileavitobmp 246 if fi.format=='LGBIT': 247 # LG phones may return a proprietary wallpaper media file, LGBIT 248 return file, conversions.convertfilelgbittobmp 249 if fi.format=='3GPP2': 250 # video format, can't yet display the first frame. 251 return file, lambda name: None 252 return file, wx.Image 253 return self._dummy_image_filename, wx.Image
254
255 - def GetFileInfoString(self, string):
256 return fileinfo.identify_imagestring(string)
257
258 - def GetFileInfo(self, filename):
260
261 - def OnAddFiles(self, filenames):
262 for file in filenames: 263 file_stat=os.stat(file) 264 mtime=file_stat.st_mtime 265 if self._raw_image: 266 targetfilename=self.get_media_name_from_filename(file) 267 data=open(file, 'rb').read() 268 self.AddToIndex(targetfilename, self.active_section, data, self._data, mtime) 269 else: 270 # :::TODO:: do i need to handle bci specially here? 271 # The proper way to handle custom image types, e.g. BCI and LGBIT, 272 # is to add a wx.ImageHandler for it. Unfortunately wx.Image_AddHandler 273 # is broken in the current wxPython, so . . . 274 fi=self.GetFileInfo(file) 275 if fi is not None and fi.format=='LGBIT': 276 img=conversions.convertfilelgbittobmp(file) 277 elif fi and fi.format=='3GPP2': 278 # 3g2 video file, no scaling, just add 279 targetfilename=self.get_media_name_from_filename(file) 280 data=open(file, 'rb').read() 281 self.AddToIndex(targetfilename, self.active_section, data, self._data, mtime) 282 continue 283 else: 284 img=wx.Image(file) 285 if not img.Ok(): 286 guihelper.MessageDialog(self, "Failed to understand the image in '"+file+"'", 287 "Image not understood", style=wx.OK|wx.ICON_ERROR) 288 continue 289 self.OnAddImage(img,file,refresh=False, timestamp=mtime) 290 self.OnRefresh()
291
292 - def ReplaceContents(self, name, origin, new_file_name):
293 """Replace the contents of 'file_name' by the contents of 294 'new_file_name' by going through the image converter dialog 295 """ 296 fi=self.GetFileInfo(new_file_name) 297 if fi is not None and fi.format=='LGBIT': 298 img=conversions.convertfilelgbittobmp(new_file_name) 299 else: 300 img=wx.Image(new_file_name) 301 if not img.Ok(): 302 guihelper.MessageDialog(self, "Failed to understand the image in '"+new_file_name+"'", 303 "Image not understood", style=wx.OK|wx.ICON_ERROR) 304 return 305 with guihelper.WXDialogWrapper(ImagePreviewDialog(self, img, self.mainwindow.phoneprofile, self.active_section), 306 True) as (dlg, retcode): 307 if retcode==wx.ID_OK: 308 img=dlg.GetResultImage() 309 imgparams=dlg.GetResultParams() 310 # ::TODO:: temporary hack - this should really be an imgparam 311 extension={'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'}[imgparams['format']] 312 313 res=getattr(self, "saveimage_"+imgparams['format'])(img, imgparams) 314 if not res: 315 guihelper.MessageDialog(self, "Failed to convert the image in '"+new_file_name+"'", 316 "Image not converted", style=wx.OK|wx.ICON_ERROR) 317 self.AddToIndex(name, origin, res, self._data)
318
319 - def OnAddImage(self, img, file, refresh=True, timestamp=None):
320 # ::TODO:: if file is None, find next basename in our directory for 321 # clipboard99 where 99 is next unused number 322 323 with guihelper.WXDialogWrapper(ImagePreviewDialog(self, img, self.mainwindow.phoneprofile, self.active_section), 324 True) as (dlg, retcode): 325 if retcode==wx.ID_OK: 326 img=dlg.GetResultImage() 327 imgparams=dlg.GetResultParams() 328 origin=self.active_section 329 # if we modified the image update the timestamp 330 if not dlg.skip: 331 timestamp=int(time.time()) 332 333 # ::TODO:: temporary hack - this should really be an imgparam 334 extension={'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'}[imgparams['format']] 335 336 # munge name 337 targetfilename=self.get_media_name_from_filename(file, extension) 338 339 res=getattr(self, "saveimage_"+imgparams['format'])(img, imgparams) 340 if not res: 341 guihelper.MessageDialog(self, "Failed to convert the image in '"+file+"'", 342 "Image not converted", style=wx.OK|wx.ICON_ERROR) 343 return 344 345 self.AddToIndex(targetfilename, origin, res, self._data, timestamp) 346 if refresh: 347 self.OnRefresh()
348
349 - def saveimage_BMP(self, img, imgparams):
350 if img.ComputeHistogram(wx.ImageHistogram())<=236: # quantize only does 236 or less 351 img.SetOptionInt(wx.IMAGE_OPTION_BMP_FORMAT, wx.BMP_8BPP) 352 with common.usetempfile('bmp') as f: 353 if img.SaveFile(f, wx.BITMAP_TYPE_BMP): 354 return file(f, 'rb').read() 355 return False
356
357 - def saveimage_JPEG(self, img, imgparams):
358 img.SetOptionInt("quality", 100) 359 with common.usetempfile('jpg') as f: 360 if img.SaveFile(f, wx.BITMAP_TYPE_JPEG): 361 return file(f, 'rb').read() 362 return False
363
364 - def saveimage_PNG(self, img, imgparams):
365 # ::TODO:: this is where the file size constraints should be examined 366 # and obeyed 367 with common.usetempfile('png') as f: 368 if img.SaveFile(f, wx.BITMAP_TYPE_PNG): 369 return file(f, 'rb').read() 370 return False
371
372 - def versionupgrade(self, dict, version):
373 """Upgrade old data format read from disk 374 375 @param dict: The dict that was read in 376 @param version: version number of the data on disk 377 """ 378 379 # version 0 to 1 upgrade 380 if version==0: 381 version=1 # they are the same 382 383 # 1 to 2 etc 384 if version==1: 385 print "converting to version 2" 386 version=2 387 d={} 388 input=dict.get(self.database_key, {}) 389 for i in input: 390 d[i]={'name': input[i]} 391 dict[self.database_key]=d 392 return dict
393
394 -class WallpaperPreview(wx.PyWindow):
395
396 - def __init__(self, parent, image=None, id=1, size=wx.DefaultSize, pos=wx.DefaultPosition, style=0):
397 wx.PyWindow.__init__(self, parent, id=id, size=size, pos=pos, style=style|wx.FULL_REPAINT_ON_RESIZE) 398 self.bg=wx.Brush(parent.GetBackgroundColour()) 399 self._bufbmp=None 400 401 wx.EVT_ERASE_BACKGROUND(self, lambda evt: None) 402 wx.EVT_PAINT(self, self.OnPaint) 403 404 self.SetImage(image)
405
406 - def SetImage(self, name, origin=None):
407 if name is None: 408 self.theimage=None 409 else: 410 self.theimage=thewallpapermanager.GetImage(name, origin) 411 self.thesizedbitmap=None 412 self.Refresh(False)
413
414 - def OnPaint(self, _):
415 sz=self.GetClientSize() 416 if self._bufbmp is None or sz.width>self._bufbmp.GetWidth() or sz.height>self._bufbmp.GetHeight(): 417 self._bufbmp=wx.EmptyBitmap((sz.width+64)&~8, (sz.height+64)&~8) 418 dc=wx.BufferedPaintDC(self, self._bufbmp, style=wx.BUFFER_VIRTUAL_AREA) 419 dc.SetBackground(self.bg) 420 dc.Clear() 421 if self.theimage is None: return 422 # work out what size the scaled bitmap should be to retain its aspect ratio and fit within sz 423 sfactorw=float(sz.width)/self.theimage.GetWidth() 424 sfactorh=float(sz.height)/self.theimage.GetHeight() 425 sfactor=min(sfactorw,sfactorh) 426 newwidth=int(self.theimage.GetWidth()*sfactor) 427 newheight=int(self.theimage.GetHeight()*sfactor) 428 if self.thesizedbitmap is None or self.thesizedbitmap.GetWidth()!=newwidth or \ 429 self.thesizedbitmap.GetHeight()!=newheight: 430 self.thesizedbitmap=self.theimage.Scale(newwidth, newheight).ConvertToBitmap() 431 dc.DrawBitmap(self.thesizedbitmap, sz.width/2-newwidth/2, sz.height/2-newheight/2, True)
432 433 434 435
436 -def ScaleImageIntoBitmap(img, usewidth, useheight, bgcolor=None, valign="center"):
437 """Scales the image and returns a bitmap 438 439 @param usewidth: the width of the new image 440 @param useheight: the height of the new image 441 @param bgcolor: the background colour as a string ("ff0000" is red etc). If this 442 is none then the background is made transparent""" 443 if bgcolor is None: 444 bitmap=wx.EmptyBitmap(usewidth, useheight, 24) # have to use 24 bit for transparent background 445 else: 446 bitmap=wx.EmptyBitmap(usewidth, useheight) 447 mdc=wx.MemoryDC() 448 mdc.SelectObject(bitmap) 449 # scale the source. 450 sfactorw=usewidth*1.0/img.GetWidth() 451 sfactorh=useheight*1.0/img.GetHeight() 452 sfactor=min(sfactorw,sfactorh) # preserve aspect ratio 453 newwidth=int(img.GetWidth()*sfactor/1.0) 454 newheight=int(img.GetHeight()*sfactor/1.0) 455 456 img.Rescale(newwidth, newheight) 457 # deal with bgcolor/transparency 458 if bgcolor is not None: 459 transparent=None 460 assert len(bgcolor)==6 461 red=int(bgcolor[0:2],16) 462 green=int(bgcolor[2:4],16) 463 blue=int(bgcolor[4:6],16) 464 mdc.SetBackground(wx.TheBrushList.FindOrCreateBrush(wx.Colour(red,green,blue), wx.SOLID)) 465 else: 466 transparent=wx.Colour(*(img.FindFirstUnusedColour()[1:])) 467 mdc.SetBackground(wx.TheBrushList.FindOrCreateBrush(transparent, wx.SOLID)) 468 mdc.Clear() 469 mdc.SelectObject(bitmap) 470 # figure where to place image to centre it 471 posx=usewidth-(usewidth+newwidth)/2 472 if valign in ("top", "clip"): 473 posy=0 474 elif valign=="center": 475 posy=useheight-(useheight+newheight)/2 476 else: 477 assert False, "bad valign "+valign 478 posy=0 479 # draw the image 480 mdc.DrawBitmap(img.ConvertToBitmap(), posx, posy, True) 481 # clean up 482 mdc.SelectObject(wx.NullBitmap) 483 # deal with transparency 484 if transparent is not None: 485 mask=wx.Mask(bitmap, transparent) 486 bitmap.SetMask(mask) 487 if valign=="clip" and newheight!=useheight: 488 return bitmap.GetSubBitmap( (0,0,usewidth,newheight) ) 489 return bitmap
490 491 ### 492 ### Virtual filesystem where the images etc come from for the HTML stuff 493 ### 494 495 statinfo=common.statinfo 496
497 -class BPFSHandler(wx.FileSystemHandler):
498 499 CACHELOWWATER=80 500 CACHEHIGHWATER=100 501
502 - def __init__(self, wallpapermanager):
503 wx.FileSystemHandler.__init__(self) 504 self.wpm=wallpapermanager 505 self.cache={}
506
507 - def _GetCache(self, location, statinfo):
508 """Return the cached item, or None 509 510 Note that the location value includes the filename and the parameters such as width/height 511 """ 512 if statinfo is None: 513 print "bad location",location 514 return None 515 return self.cache.get( (location, statinfo), None)
516
517 - def _AddCache(self, location, statinfo, value):
518 "Add the item to the cache" 519 # we also prune it down in size if necessary 520 if len(self.cache)>=self.CACHEHIGHWATER: 521 print "BPFSHandler cache flush" 522 # random replacement - almost as good as LRU ... 523 while len(self.cache)>self.CACHELOWWATER: 524 del self.cache[random.choice(self.cache.keys())] 525 self.cache[(location, statinfo)]=value
526
527 - def CanOpen(self, location):
528 529 # The call to self.GetProtocol causes an exception if the 530 # location starts with a pathname! This typically happens 531 # when the help file is opened. So we work around that bug 532 # with this quick check. 533 534 if location.startswith("/"): 535 return False 536 proto=self.GetProtocol(location) 537 if proto=="bpimage" or proto=="bpuserimage": 538 return True 539 return False
540
541 - def OpenFile(self,filesystem,location):
542 try: 543 res=self._OpenFile(filesystem,location) 544 except: 545 res=None 546 print "Exception in getting image file - you can't do that!" 547 print common.formatexception() 548 if res is not None: 549 # we have to seek the file object back to the begining and make a new 550 # wx.FSFile each time as wxPython doesn't do the reference counting 551 # correctly 552 res[0].seek(0) 553 args=(wx.InputStream(res[0]),)+res[1:] 554 res=wx.FSFile(*args) 555 return res
556
557 - def _OpenFile(self, filesystem, location):
558 proto=self.GetProtocol(location) 559 r=self.GetRightLocation(location) 560 params=r.split(';') 561 r=params[0] 562 params=params[1:] 563 p={} 564 for param in params: 565 x=param.find('=') 566 key=str(param[:x]) 567 value=param[x+1:] 568 if key=='width' or key=='height': 569 p[key]=int(value) 570 else: 571 p[key]=value 572 if proto=="bpimage": 573 return self.OpenBPImageFile(location, r, **p) 574 elif proto=="bpuserimage": 575 return self.OpenBPUserImageFile(location, r, **p) 576 return None
577
578 - def OpenBPUserImageFile(self, location, name, **kwargs):
579 res=BPFSImageFile(self, location, img=self.wpm.GetImage(name, None), **kwargs) 580 return res
581
582 - def OpenBPImageFile(self, location, name, **kwargs):
583 f=guihelper.getresourcefile(name) 584 if not os.path.isfile(f): 585 print f,"doesn't exist" 586 return None 587 si=statinfo(f) 588 res=self._GetCache(location, si) 589 if res is not None: return res 590 res=BPFSImageFile(self, location, name=f, **kwargs) 591 self._AddCache(location, si, res) 592 return res
593
594 -def BPFSImageFile(fshandler, location, name=None, img=None, width=-1, height=-1, valign="center", bgcolor=None):
595 """Handles image files 596 597 If we have to do any conversion on the file then we return PNG 598 data. This used to be a class derived from wx.FSFile, but due to 599 various wxPython bugs it instead returns the parameters to make a 600 wx.FSFile since a new one has to be made every time. 601 """ 602 # if this is a bad or non-existing image file, use the dummy one! 603 if name is None and img is None: 604 name=guihelper.getresourcefile('wallpaper.png') 605 # special fast path if we aren't resizing or converting image 606 if img is None and width<0 and height<0: 607 mime=guihelper.getwxmimetype(name) 608 # wxPython 2.5.3 has a new bug and fails to read bmp files returned as a stream 609 if mime not in (None, "image/x-bmp"): 610 return (open(name, "rb"), location, mime, "", wx.DateTime_Now()) 611 612 if img is None: 613 img=wx.Image(name) 614 615 if width>0 and height>0: 616 b=ScaleImageIntoBitmap(img, width, height, bgcolor, valign) 617 else: 618 b=img.ConvertToBitmap() 619 620 with common.usetempfile('png') as f: 621 if not b.SaveFile(f, wx.BITMAP_TYPE_PNG): 622 raise Exception, "Saving to png failed" 623 data=open(f, "rb").read() 624 return (cStringIO.StringIO(data), location, "image/png", "", wx.DateTime_Now())
625 626
627 -class ImageCropSelect(wx.ScrolledWindow):
628
629 - def __init__(self, parent, image, previewwindow=None, id=1, resultsize=(100,100), size=wx.DefaultSize, pos=wx.DefaultPosition, style=0):
630 wx.ScrolledWindow.__init__(self, parent, id=id, size=size, pos=pos, style=style|wx.FULL_REPAINT_ON_RESIZE) 631 self.previewwindow=previewwindow 632 self.bg=wx.Brush(wx.WHITE) 633 self.parentbg=wx.Brush(parent.GetBackgroundColour()) 634 self._bufbmp=None 635 636 self.anchors=None 637 638 wx.EVT_ERASE_BACKGROUND(self, lambda evt: None) 639 wx.EVT_PAINT(self, self.OnPaint) 640 641 self.image=image 642 self.origimage=image 643 self.setresultsize(resultsize) 644 645 # cursors for outside, inside, on selection, pressing bad mouse button 646 self.cursors=[wx.StockCursor(c) for c in (wx.CURSOR_ARROW, wx.CURSOR_HAND, wx.CURSOR_SIZING, wx.CURSOR_NO_ENTRY)] 647 self.clickpoint=None 648 wx.EVT_MOTION(self, self.OnMotion) 649 wx.EVT_LEFT_DOWN(self, self.OnLeftDown) 650 wx.EVT_LEFT_UP(self, self.OnLeftUp)
651
652 - def SetPreviewWindow(self, previewwindow):
653 self.previewwindow=previewwindow
654
655 - def OnPaint(self, _):
656 sz=self.thebmp.GetWidth(), self.thebmp.GetHeight() 657 sz2=self.GetClientSize() 658 sz=max(sz[0],sz2[0])+32,max(sz[1],sz2[1])+32 659 if self._bufbmp is None or self._bufbmp.GetWidth()<sz[0] or self._bufbmp.GetHeight()<sz[1]: 660 self._bufbmp=wx.EmptyBitmap((sz[0]+64)&~8, (sz[1]+64)&~8) 661 dc=wx.BufferedPaintDC(self, self._bufbmp, style=wx.BUFFER_VIRTUAL_AREA) 662 if sz2[0]<sz[0] or sz2[1]<sz[1]: 663 dc.SetBackground(self.parentbg) 664 dc.Clear() 665 dc.DrawBitmap(self.thebmp, 0, 0, False) 666 # draw bounding box next 667 l,t,r,b=self.anchors 668 points=(l,t), (r,t), (r,b), (l,b) 669 dc.DrawLines( points+(points[0],) ) 670 for x,y in points: 671 dc.DrawRectangle(x-5, y-5, 10, 10)
672 673 OUTSIDE=0 674 INSIDE=1 675 HANDLE_LT=2 676 HANDLE_RT=3 677 HANDLE_RB=4 678 HANDLE_LB=5 679
680 - def _hittest(self, evt):
681 l,t,r,b=self.anchors 682 within=lambda x,y,l,t,r,b: l<=x<=r and t<=y<=b 683 x,y=self.CalcUnscrolledPosition(evt.GetX(), evt.GetY()) 684 for i,(ptx,pty) in enumerate(((l,t), (r,t), (r,b), (l,b))): 685 if within(x,y,ptx-5, pty-5, ptx+5,pty+5): 686 return self.HANDLE_LT+i 687 if within(x,y,l,t,r,b): 688 return self.INSIDE 689 return self.OUTSIDE
690
691 - def OnMotion(self, evt):
692 if evt.Dragging(): 693 return self.OnMotionDragging(evt) 694 self.UpdateCursor(evt)
695
696 - def UpdateCursor(self, evt):
697 ht=self._hittest(evt) 698 self.SetCursor(self.cursors[min(2,ht)])
699
700 - def OnMotionDragging(self, evt):
701 if not evt.LeftIsDown() or self.clickpoint is None: 702 self.SetCursor(self.cursors[3]) 703 return 704 xx,yy=self.CalcUnscrolledPosition(evt.GetX(), evt.GetY()) 705 deltax=xx-self.origevtpos[0] 706 deltay=yy-self.origevtpos[1] 707 708 if self.clickpoint==self.INSIDE: 709 newanchors=self.origanchors[0]+deltax, self.origanchors[1]+deltay, \ 710 self.origanchors[2]+deltax, self.origanchors[3]+deltay 711 iw=self.dimensions[0] 712 ih=self.dimensions[1] 713 # would box be out of bounds? 714 if newanchors[0]<0: 715 newanchors=0,newanchors[1], self.origanchors[2]-self.origanchors[0], newanchors[3] 716 if newanchors[1]<0: 717 newanchors=newanchors[0], 0, newanchors[2], self.origanchors[3]-self.origanchors[1] 718 if newanchors[2]>iw: 719 newanchors=iw-(self.origanchors[2]-self.origanchors[0]),newanchors[1],iw, newanchors[3] 720 if newanchors[3]>ih: 721 newanchors=newanchors[0],ih-(self.origanchors[3]-self.origanchors[1]), newanchors[2],ih 722 self.anchors=newanchors 723 self.Refresh(False) 724 self.updatepreview() 725 return 726 # work out how to do this with left top and then expand code 727 if self.clickpoint==self.HANDLE_LT: 728 aa=0,1,-1,-1 729 elif self.clickpoint==self.HANDLE_RT: 730 aa=2,1,+1,-1 731 elif self.clickpoint==self.HANDLE_RB: 732 aa=2,3,+1,+1 733 elif self.clickpoint==self.HANDLE_LB: 734 aa=0,3,-1,+1 735 else: 736 assert False, "can't get here" 737 738 na=[self.origanchors[0],self.origanchors[1],self.origanchors[2],self.origanchors[3]] 739 na[aa[0]]=na[aa[0]]+deltax 740 na[aa[1]]=na[aa[1]]+deltay 741 neww=na[2]-na[0] 742 newh=na[3]-na[1] 743 ar=float(neww)/newh 744 if ar<self.aspectratio: 745 na[aa[0]]=na[aa[0]]+(self.aspectratio*newh-neww)*aa[2] 746 elif ar>self.aspectratio: 747 na[aa[1]]=na[aa[1]]+(neww/self.aspectratio-newh)*aa[3] 748 749 # ignore if image would be smaller than 10 pixels in any direction 750 if neww<10 or newh<10: 751 return 752 # if any point is off screen, we need to fix things up 753 if na[0]<0: 754 xdiff=-na[0] 755 ydiff=xdiff/self.aspectratio 756 na[0]=0 757 na[1]+=ydiff 758 if na[1]<0: 759 ydiff=-na[1] 760 xdiff=ydiff*self.aspectratio 761 na[1]=0 762 na[0]-=xdiff 763 if na[2]>self.dimensions[0]: 764 xdiff=na[2]-self.dimensions[0] 765 ydiff=xdiff/self.aspectratio 766 na[2]=na[2]-xdiff 767 na[3]=na[3]-ydiff 768 if na[3]>self.dimensions[1]: 769 ydiff=na[3]-self.dimensions[1] 770 xdiff=ydiff*self.aspectratio 771 na[2]=na[2]-xdiff 772 na[3]=na[3]-ydiff 773 if na[0]<0 or na[1]<0 or na[2]>self.dimensions[0] or na[3]>self.dimensions[1]: 774 print "offscreen fixup not written yet" 775 return 776 777 # work out aspect ratio 778 self.anchors=na 779 self.Refresh(False) 780 self.updatepreview() 781 return
782 783
784 - def OnLeftDown(self, evt):
785 ht=self._hittest(evt) 786 if ht==self.OUTSIDE: 787 self.SetCursor(self.cursors[3]) 788 return 789 self.clickpoint=ht 790 xx,yy=self.CalcUnscrolledPosition(evt.GetX(), evt.GetY()) 791 self.origevtpos=xx,yy 792 self.origanchors=self.anchors
793
794 - def OnLeftUp(self, evt):
795 self.clickpoint=None 796 self.UpdateCursor(evt)
797
798 - def setlbcolour(self, colour):
799 self.bg=wx.Brush(colour) 800 self.remakebitmap()
801
802 - def SetZoom(self, factor):
803 curzoom=float(self.image.GetWidth())/self.origimage.GetWidth() 804 self.anchors=[a*factor/curzoom for a in self.anchors] 805 self.image=self.origimage.Scale(self.origimage.GetWidth()*factor, self.origimage.GetHeight()*factor) 806 self.setresultsize(self.resultsize)
807
808 - def setresultsize(self, (w,h)):
809 self.resultsize=w,h 810 self.aspectratio=ratio=float(w)/h 811 imgratio=float(self.image.GetWidth())/self.image.GetHeight() 812 813 neww=self.image.GetWidth() 814 newh=self.image.GetHeight() 815 if imgratio<ratio: 816 neww*=ratio/imgratio 817 elif imgratio>ratio: 818 newh*=imgratio/ratio 819 820 # ensure a minimum size 821 neww=max(neww, 50) 822 newh=max(newh, 50) 823 824 # update anchors if never set 825 if self.anchors==None: 826 self.anchors=0.1 * neww, 0.1 * newh, 0.9 * neww, 0.9 * newh 827 828 # fixup anchors 829 l,t,r,b=self.anchors 830 l=min(neww-40, l) 831 r=min(neww-10, r) 832 if r-l<20: r=40 833 t=min(newh-40, t) 834 b=min(newh-10, b) 835 if b-t<20: b=40 836 aratio=float(r-l)/(b-t) 837 if aratio<ratio: 838 b=t+(r-l)/ratio 839 elif aratio>ratio: 840 r=l+(b-t)*ratio 841 self.anchors=l,t,r,b 842 843 self.dimensions=neww,newh 844 self.thebmp=wx.EmptyBitmap(neww, newh) 845 846 self.remakebitmap()
847 848
849 - def remakebitmap(self):
850 w,h=self.dimensions 851 dc=wx.MemoryDC() 852 dc.SelectObject(self.thebmp) 853 dc.SetBackground(self.bg) 854 dc.Clear() 855 dc.DrawBitmap(self.image.ConvertToBitmap(), w/2-self.image.GetWidth()/2, h/2-self.image.GetHeight()/2, True) 856 dc.SelectObject(wx.NullBitmap) 857 self.imageofthebmp=None 858 self.SetVirtualSize( (w, h) ) 859 self.SetScrollRate(1,1) 860 861 self.updatepreview() 862 self.Refresh(False)
863 864 # updating the preview is expensive so it is done on demand. We 865 # tell the preview window there has been an update and it calls 866 # back from its paint method
867 - def updatepreview(self):
868 if self.previewwindow: 869 self.previewwindow.SetUpdated(self.GetPreview)
870
871 - def GetPreview(self):
872 w,h=self.resultsize 873 l,t,r,b=self.anchors 874 sub=self.thebmp.GetSubBitmap( (l,t,(r-l),(b-t)) ) 875 sub=sub.ConvertToImage() 876 sub.Rescale(w,h) 877 return sub.ConvertToBitmap()
878
879 -class ImagePreview(wx.PyWindow):
880
881 - def __init__(self, parent):
882 wx.PyWindow.__init__(self, parent) 883 wx.EVT_ERASE_BACKGROUND(self, lambda evt: None) 884 wx.EVT_PAINT(self, self.OnPaint) 885 self.bmp=wx.EmptyBitmap(1,1) 886 self.updater=None
887
888 - def SetUpdated(self, updater):
889 self.updater=updater 890 self.Refresh(True)
891
892 - def OnPaint(self, _):
893 if self.updater is not None: 894 self.bmp=self.updater() 895 self.updater=None 896 dc=wx.PaintDC(self) 897 dc.DrawBitmap(self.bmp, 0, 0, False)
898 899
900 -class ImagePreviewDialog(wx.Dialog):
901 902 SCALES=[ (0.25, "1/4"), 903 (0.5, "1/2"), 904 (1, "1"), 905 (2, "2"), 906 (4, "4")] 907
908 - def __init__(self, parent, image, phoneprofile, origin):
909 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) 910 self.phoneprofile=phoneprofile 911 self.image=image 912 self.skip=False 913 self.origin=origin 914 self.target='' 915 916 self.vbsouter=wx.BoxSizer(wx.VERTICAL) 917 self.SetupControls(self.vbsouter) 918 self.OnTargetSelect(0) 919 920 wx.EVT_BUTTON(self, wx.ID_HELP, lambda _: 921 wx.GetApp().displayhelpid(helpids.ID_DLG_IMAGEPREVIEW)) 922 923 self.SetSizer(self.vbsouter) 924 self.vbsouter.Fit(self) 925 926 import guiwidgets 927 guiwidgets.set_size("wallpaperpreview", self, 80, 1.0)
928
929 - def SetupControls(self, vbsouter):
930 vbsouter.Clear(True) 931 hbs=wx.BoxSizer(wx.HORIZONTAL) 932 self.cropselect=ImageCropSelect(self, self.image) 933 934 self.vbs=wx.BoxSizer(wx.VERTICAL) 935 self.colourselect=wx.lib.colourselect.ColourSelect(self, wx.NewId(), "Background Color ...", (255,255,255)) 936 self.vbs.Add(self.colourselect, 0, wx.ALL|wx.EXPAND, 5) 937 wx.lib.colourselect.EVT_COLOURSELECT(self, self.colourselect.GetId(), self.OnBackgroundColour) 938 self.vbs.Add(wx.StaticText(self, -1, "Target"), 0, wx.ALL, 5) 939 self.targetbox=wx.ListBox(self, -1, size=(-1,100)) 940 self.vbs.Add(self.targetbox, 0, wx.EXPAND|wx.ALL, 5) 941 self.vbs.Add(wx.StaticText(self, -1, "Scale"), 0, wx.ALL, 5) 942 943 for one,(s,_) in enumerate(self.SCALES): 944 if s==1: break 945 self.slider=wx.Slider(self, -1, one, 0, len(self.SCALES)-1, style=wx.HORIZONTAL|wx.SL_AUTOTICKS) 946 self.vbs.Add(self.slider, 0, wx.ALL|wx.EXPAND, 5) 947 self.zoomlabel=wx.StaticText(self, -1, self.SCALES[one][1]) 948 self.vbs.Add(self.zoomlabel, 0, wx.ALL|wx.ALIGN_CENTRE_HORIZONTAL, 5) 949 950 self.vbs.Add(wx.StaticText(self, -1, "Preview"), 0, wx.ALL, 5) 951 self.imagepreview=ImagePreview(self) 952 self.cropselect.SetPreviewWindow(self.imagepreview) 953 self.vbs.Add(self.imagepreview, 0, wx.ALL, 5) 954 955 hbs.Add(self.vbs, 0, wx.ALL, 5) 956 hbs.Add(self.cropselect, 1, wx.ALL|wx.EXPAND, 5) 957 958 vbsouter.Add(hbs, 1, wx.EXPAND|wx.ALL, 5) 959 960 vbsouter.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5) 961 vbsouter.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 962 963 wx.EVT_SCROLL(self, self.SetZoom) 964 wx.EVT_LISTBOX(self, self.targetbox.GetId(), self.OnTargetSelect) 965 wx.EVT_LISTBOX_DCLICK(self, self.targetbox.GetId(), self.OnTargetSelect) 966 967 self.OnOriginSelect()
968
969 - def ShowModal(self):
970 # see if the image size is already correct 971 i_w, i_h=self.image.GetSize() 972 for v in self.targets: 973 w,h=self.targets[v]['width'],self.targets[v]['height'] 974 if abs(i_w-w) < 5 and abs(i_h-h) < 5: 975 self.skip=True 976 return wx.ID_OK 977 res=wx.Dialog.ShowModal(self) 978 import guiwidgets 979 guiwidgets.save_size("wallpaperpreview", self.GetRect()) 980 return res
981
982 - def SetZoom(self, evt):
983 self.cropselect.SetZoom(self.SCALES[evt.GetPosition()][0]) 984 self.zoomlabel.SetLabel(self.SCALES[evt.GetPosition()][1]) 985 return
986
987 - def OnBackgroundColour(self, evt):
988 self.cropselect.setlbcolour(evt.GetValue())
989
990 - def OnOriginSelect(self):
991 self.targets=self.phoneprofile.GetTargetsForImageOrigin(self.origin) 992 keys=self.targets.keys() 993 keys.sort() 994 self.targetbox.Set(keys) 995 if self.target in keys: 996 self.targetbox.SetSelection(keys.index(self.target)) 997 else: 998 self.targetbox.SetSelection(0)
999
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