controller.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. """
  2. @package animation.controller
  3. @brief Animations management
  4. Classes:
  5. - controller::AnimationController
  6. (C) 2013 by the GRASS Development Team
  7. This program is free software under the GNU General Public License
  8. (>=v2). Read the file COPYING that comes with GRASS for details.
  9. @author Anna Petrasova <kratochanna gmail.com>
  10. """
  11. import os
  12. import wx
  13. from core.gcmd import GException, GError, GMessage
  14. from grass.imaging import writeAvi, writeGif, writeIms, writeSwf
  15. from core.gthread import gThread
  16. from core.settings import UserSettings
  17. from gui_core.wrap import EmptyImage, ImageFromBitmap
  18. from animation.temporal_manager import TemporalManager
  19. from animation.dialogs import InputDialog, EditDialog, ExportDialog
  20. from animation.utils import TemporalMode, TemporalType, Orientation, RenderText, WxImageToPil, \
  21. sampleCmdMatrixAndCreateNames, layerListToCmdsMatrix, HashCmds, getCpuCount
  22. from animation.data import AnimationData
  23. class AnimationController(wx.EvtHandler):
  24. def __init__(self, frame, sliders, animations, mapwindows,
  25. provider, bitmapPool, mapFilesPool):
  26. wx.EvtHandler.__init__(self)
  27. self.mapwindows = mapwindows
  28. self.frame = frame
  29. self.sliders = sliders
  30. self.slider = self.sliders['temporal']
  31. self.animationToolbar = None
  32. self.temporalMode = None
  33. self.animationData = []
  34. self.timer = wx.Timer(self, id=wx.ID_ANY)
  35. self.animations = animations
  36. self.bitmapPool = bitmapPool
  37. self.mapFilesPool = mapFilesPool
  38. self.bitmapProvider = provider
  39. for anim, win in zip(self.animations, self.mapwindows):
  40. anim.SetCallbackUpdateFrame(
  41. lambda index, dataId, win=win: self.UpdateFrame(
  42. index, win, dataId))
  43. anim.SetCallbackEndAnimation(
  44. lambda index, dataId, win=win: self.UpdateFrameEnd(
  45. index, win, dataId))
  46. anim.SetCallbackOrientationChanged(
  47. self.OrientationChangedInReverseMode)
  48. for slider in self.sliders.values():
  49. slider.SetCallbackSliderChanging(self.SliderChanging)
  50. slider.SetCallbackSliderChanged(self.SliderChanged)
  51. slider.SetCallbackFrameIndexChanged(self.ChangeFrame)
  52. self.runAfterReleasingSlider = None
  53. self.temporalManager = TemporalManager()
  54. self.Bind(wx.EVT_TIMER, self.OnTimerTick, self.timer)
  55. self.timeTick = 200
  56. self._dialogs = {}
  57. def SetAnimationToolbar(self, toolbar):
  58. self.animationToolbar = toolbar
  59. def GetTimeTick(self):
  60. return self._timeTick
  61. def SetTimeTick(self, value):
  62. self._timeTick = value
  63. if self.timer.IsRunning():
  64. self.timer.Stop()
  65. self.timer.Start(self._timeTick)
  66. self.DisableSliderIfNeeded()
  67. timeTick = property(fget=GetTimeTick, fset=SetTimeTick)
  68. def OnTimerTick(self, event):
  69. for anim in self.animations:
  70. anim.Update()
  71. def StartAnimation(self):
  72. # if self.timer.IsRunning():
  73. # self.timer.Stop()
  74. for anim in self.animations:
  75. if self.timer.IsRunning():
  76. anim.NextFrameIndex()
  77. anim.Start()
  78. if not self.timer.IsRunning():
  79. self.timer.Start(self.timeTick)
  80. self.DisableSliderIfNeeded()
  81. def PauseAnimation(self, paused):
  82. if paused:
  83. if self.timer.IsRunning():
  84. self.timer.Stop()
  85. self.DisableSliderIfNeeded()
  86. else:
  87. if not self.timer.IsRunning():
  88. self.timer.Start(self.timeTick)
  89. self.DisableSliderIfNeeded()
  90. for anim in self.animations:
  91. anim.Pause(paused)
  92. def EndAnimation(self):
  93. if self.timer.IsRunning():
  94. self.timer.Stop()
  95. self.DisableSliderIfNeeded()
  96. for anim in self.animations:
  97. anim.Stop()
  98. def UpdateFrameEnd(self, index, win, dataId):
  99. if self.timer.IsRunning():
  100. self.timer.Stop()
  101. self.DisableSliderIfNeeded()
  102. self.animationToolbar.Stop()
  103. self.UpdateFrame(index, win, dataId)
  104. def UpdateFrame(self, index, win, dataId):
  105. bitmap = self.bitmapProvider.GetBitmap(dataId)
  106. if not UserSettings.Get(group='animation', key='temporal',
  107. subkey=['nodata', 'enable']):
  108. if dataId is not None:
  109. win.DrawBitmap(bitmap)
  110. else:
  111. win.DrawBitmap(bitmap)
  112. self.slider.UpdateFrame(index)
  113. def SliderChanging(self, index):
  114. if self.runAfterReleasingSlider is None:
  115. self.runAfterReleasingSlider = self.timer.IsRunning()
  116. self.PauseAnimation(True)
  117. self.ChangeFrame(index)
  118. def SliderChanged(self):
  119. if self.runAfterReleasingSlider:
  120. self.PauseAnimation(False)
  121. self.runAfterReleasingSlider = None
  122. def ChangeFrame(self, index):
  123. for anim in self.animations:
  124. anim.FrameChangedFromOutside(index)
  125. def DisableSliderIfNeeded(self):
  126. if self.timer.IsRunning() and self._timeTick < 100:
  127. self.slider.EnableSlider(False)
  128. else:
  129. self.slider.EnableSlider(True)
  130. def OrientationChangedInReverseMode(self, mode):
  131. if mode == Orientation.FORWARD:
  132. self.animationToolbar.PlayForward()
  133. elif mode == Orientation.BACKWARD:
  134. self.animationToolbar.PlayBack()
  135. def SetReplayMode(self, mode):
  136. for anim in self.animations:
  137. anim.replayMode = mode
  138. def SetOrientation(self, mode):
  139. for anim in self.animations:
  140. anim.orientation = mode
  141. def SetTemporalMode(self, mode):
  142. self._temporalMode = mode
  143. def GetTemporalMode(self):
  144. return self._temporalMode
  145. temporalMode = property(fget=GetTemporalMode, fset=SetTemporalMode)
  146. def GetTimeGranularity(self):
  147. if self.temporalMode == TemporalMode.TEMPORAL:
  148. return self.temporalManager.GetGranularity()
  149. return None
  150. def UpdateAnimations(self):
  151. """Used sofar for updating slider time labels
  152. after change of format"""
  153. self._setAnimations()
  154. def EditAnimations(self):
  155. # running = False
  156. # if self.timer.IsRunning():
  157. # running = True
  158. self.EndAnimation()
  159. dlg = EditDialog(
  160. parent=self.frame,
  161. evalFunction=self.EvaluateInput,
  162. animationData=self.animationData,
  163. maxAnimations=len(
  164. self.animations))
  165. dlg.CenterOnParent()
  166. if dlg.ShowModal() == wx.ID_CANCEL:
  167. dlg.Destroy()
  168. return
  169. self.animationData, self.temporalMode, self.temporalManager = dlg.GetResult()
  170. dlg.Destroy()
  171. self._setAnimations()
  172. def AddAnimation(self):
  173. # check if we can add more animations
  174. found = False
  175. indices = [anim.windowIndex for anim in self.animationData]
  176. for windowIndex in range(len(self.animations)):
  177. if windowIndex not in indices:
  178. found = True
  179. break
  180. if not found:
  181. GMessage(
  182. parent=self.frame,
  183. message=_("Maximum number of animations is %s.") %
  184. len(self.animations))
  185. return
  186. # running = False
  187. # if self.timer.IsRunning():
  188. # running = True
  189. self.EndAnimation()
  190. # self.PauseAnimation(True)
  191. animData = AnimationData()
  192. # number of active animations
  193. animationIndex = len(
  194. [anim for anim in self.animations if anim.IsActive()])
  195. animData.SetDefaultValues(windowIndex, animationIndex)
  196. dlg = InputDialog(
  197. parent=self.frame,
  198. mode='add',
  199. animationData=animData)
  200. dlg.CenterOnParent()
  201. if dlg.ShowModal() == wx.ID_CANCEL:
  202. dlg.UnInit()
  203. dlg.Destroy()
  204. return
  205. dlg.Destroy()
  206. # check compatibility
  207. if animData.windowIndex in indices:
  208. GMessage(
  209. parent=self.frame, message=_(
  210. "More animations are using one window."
  211. " Please select different window for each animation."))
  212. return
  213. try:
  214. temporalMode, tempManager = self.EvaluateInput(
  215. self.animationData + [animData])
  216. except GException as e:
  217. GError(parent=self.frame, message=e.value, showTraceback=False)
  218. return
  219. # if ok, set temporal mode
  220. self.temporalMode = temporalMode
  221. self.temporalManager = tempManager
  222. # add data
  223. windowIndex = animData.windowIndex
  224. self.animationData.append(animData)
  225. self._setAnimations()
  226. def SetAnimations(self, layerLists):
  227. """Set animation data directly.
  228. :param layerLists: list of layerLists
  229. """
  230. try:
  231. animationData = []
  232. for i in range(len(self.animations)):
  233. if layerLists[i]:
  234. anim = AnimationData()
  235. anim.SetDefaultValues(i, i)
  236. anim.SetLayerList(layerLists[i])
  237. animationData.append(anim)
  238. except (GException, ValueError, IOError) as e:
  239. GError(parent=self.frame, message=str(e),
  240. showTraceback=False, caption=_("Invalid input"))
  241. return
  242. try:
  243. temporalMode, tempManager = self.EvaluateInput(animationData)
  244. except GException as e:
  245. GError(parent=self.frame, message=e.value, showTraceback=False)
  246. return
  247. self.animationData = animationData
  248. self.temporalManager = tempManager
  249. self.temporalMode = temporalMode
  250. self._setAnimations()
  251. def _setAnimations(self):
  252. indices = [anim.windowIndex for anim in self.animationData]
  253. self._updateWindows(activeIndices=indices)
  254. if self.temporalMode == TemporalMode.TEMPORAL:
  255. timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
  256. else:
  257. timeLabels, mapNamesDict = None, None
  258. for anim in self.animationData:
  259. if anim.viewMode == '2d':
  260. anim.cmdMatrix = layerListToCmdsMatrix(anim.layerList)
  261. else:
  262. anim.cmdMatrix = [(cmd,)
  263. for cmd in anim.GetNvizCommands()
  264. ['commands']]
  265. self._updateSlider(timeLabels=timeLabels)
  266. self._updateAnimations(
  267. activeIndices=indices,
  268. mapNamesDict=mapNamesDict)
  269. self._updateBitmapData()
  270. # if running:
  271. # self.PauseAnimation(False)
  272. # # self.StartAnimation()
  273. # else:
  274. self.EndAnimation()
  275. def _updateSlider(self, timeLabels=None):
  276. if self.temporalMode == TemporalMode.NONTEMPORAL:
  277. self.frame.SetSlider('nontemporal')
  278. self.slider = self.sliders['nontemporal']
  279. frameCount = self.animationData[0].mapCount
  280. self.slider.SetFrames(frameCount)
  281. elif self.temporalMode == TemporalMode.TEMPORAL:
  282. self.frame.SetSlider('temporal')
  283. self.slider = self.sliders['temporal']
  284. self.slider.SetTemporalType(self.temporalManager.temporalType)
  285. self.slider.SetFrames(timeLabels)
  286. else:
  287. self.frame.SetSlider(None)
  288. self.slider = None
  289. def _updateAnimations(self, activeIndices, mapNamesDict=None):
  290. if self.temporalMode == TemporalMode.NONTEMPORAL:
  291. for i in range(len(self.animations)):
  292. if i not in activeIndices:
  293. self.animations[i].SetActive(False)
  294. continue
  295. anim = [anim for anim in self.animationData
  296. if anim.windowIndex == i][0]
  297. w, h = self.mapwindows[i].GetClientSize()
  298. regions = anim.GetRegions(w, h)
  299. if anim.viewMode == "3d":
  300. regions = [None] * len(regions)
  301. self.animations[i].SetFrames(
  302. [HashCmds(cmdList, region) for cmdList,
  303. region in zip(anim.cmdMatrix, regions)])
  304. self.animations[i].SetActive(True)
  305. else:
  306. for i in range(len(self.animations)):
  307. if i not in activeIndices:
  308. self.animations[i].SetActive(False)
  309. continue
  310. anim = [anim for anim in self.animationData
  311. if anim.windowIndex == i][0]
  312. w, h = self.mapwindows[i].GetClientSize()
  313. regions = anim.GetRegions(w, h)
  314. if anim.viewMode == "3d":
  315. regions = [None] * len(regions)
  316. identifiers = sampleCmdMatrixAndCreateNames(
  317. anim.cmdMatrix, mapNamesDict[
  318. anim.firstStdsNameType[0]], regions)
  319. self.animations[i].SetFrames(identifiers)
  320. self.animations[i].SetActive(True)
  321. def _updateWindows(self, activeIndices):
  322. # add or remove window
  323. for windowIndex in range(len(self.animations)):
  324. if not self.frame.IsWindowShown(
  325. windowIndex) and windowIndex in activeIndices:
  326. self.frame.AddWindow(windowIndex)
  327. elif self.frame.IsWindowShown(windowIndex) and windowIndex not in activeIndices:
  328. self.frame.RemoveWindow(windowIndex)
  329. def _updateBitmapData(self):
  330. # unload previous data
  331. self.bitmapProvider.Unload()
  332. # load new data
  333. for animData in self.animationData:
  334. if animData.viewMode == '2d':
  335. self._set2DData(animData)
  336. else:
  337. self._load3DData(animData)
  338. self._loadLegend(animData)
  339. color = UserSettings.Get(
  340. group='animation',
  341. key='bgcolor',
  342. subkey='color')
  343. cpus = UserSettings.Get(
  344. group='animation',
  345. key='nprocs',
  346. subkey='value')
  347. self.bitmapProvider.Load(nprocs=cpus, bgcolor=color)
  348. # clear pools
  349. self.bitmapPool.Clear()
  350. self.mapFilesPool.Clear()
  351. def _set2DData(self, animationData):
  352. opacities = [
  353. layer.opacity for layer in animationData.layerList
  354. if layer.active]
  355. w, h = self.mapwindows[animationData.GetWindowIndex()].GetClientSize()
  356. regions = animationData.GetRegions(w, h)
  357. self.bitmapProvider.SetCmds(
  358. animationData.cmdMatrix, opacities, regions)
  359. def _load3DData(self, animationData):
  360. nviz = animationData.GetNvizCommands()
  361. self.bitmapProvider.SetCmds3D(nviz['commands'], nviz['region'])
  362. def _loadLegend(self, animationData):
  363. if animationData.legendCmd:
  364. try:
  365. bitmap = self.bitmapProvider.LoadOverlay(
  366. animationData.legendCmd)
  367. try:
  368. from PIL import Image
  369. for param in animationData.legendCmd:
  370. if param.startswith('at'):
  371. b, t, l, r = param.split('=')[1].split(',')
  372. x, y = float(l) / 100., 1 - float(t) / 100.
  373. break
  374. except ImportError:
  375. x, y = 0, 0
  376. self.mapwindows[
  377. animationData.windowIndex].SetOverlay(
  378. bitmap, x, y)
  379. except GException:
  380. GError(message=_("Failed to display legend."))
  381. else:
  382. self.mapwindows[animationData.windowIndex].ClearOverlay()
  383. def EvaluateInput(self, animationData):
  384. stds = 0
  385. maps = 0
  386. mapCount = set()
  387. tempManager = None
  388. windowIndex = []
  389. for anim in animationData:
  390. for layer in anim.layerList:
  391. if layer.active and hasattr(layer, 'maps'):
  392. if layer.mapType in ('strds', 'stvds', 'str3ds'):
  393. stds += 1
  394. else:
  395. maps += 1
  396. mapCount.add(len(layer.maps))
  397. windowIndex.append(anim.windowIndex)
  398. if maps and stds:
  399. temporalMode = TemporalMode.NONTEMPORAL
  400. elif maps:
  401. temporalMode = TemporalMode.NONTEMPORAL
  402. elif stds:
  403. temporalMode = TemporalMode.TEMPORAL
  404. else:
  405. temporalMode = None
  406. if temporalMode == TemporalMode.NONTEMPORAL:
  407. if len(mapCount) > 1:
  408. raise GException(
  409. _("Inconsistent number of maps, please check input data."))
  410. elif temporalMode == TemporalMode.TEMPORAL:
  411. tempManager = TemporalManager()
  412. # these raise GException:
  413. for anim in animationData:
  414. tempManager.AddTimeSeries(*anim.firstStdsNameType)
  415. message = tempManager.EvaluateInputData()
  416. if message:
  417. GMessage(parent=self.frame, message=message)
  418. return temporalMode, tempManager
  419. def Reload(self):
  420. self.EndAnimation()
  421. color = UserSettings.Get(
  422. group='animation',
  423. key='bgcolor',
  424. subkey='color')
  425. cpus = UserSettings.Get(
  426. group='animation',
  427. key='nprocs',
  428. subkey='value')
  429. self.bitmapProvider.Load(nprocs=cpus, bgcolor=color, force=True)
  430. self.EndAnimation()
  431. def Export(self):
  432. if not self.animationData:
  433. GMessage(parent=self.frame, message=_("No animation to export."))
  434. return
  435. if 'export' in self._dialogs:
  436. self._dialogs['export'].Show()
  437. self._dialogs['export'].Raise()
  438. else:
  439. dlg = ExportDialog(self.frame, temporal=self.temporalMode,
  440. timeTick=self.timeTick)
  441. dlg.CenterOnParent()
  442. dlg.doExport.connect(self._export)
  443. self._dialogs['export'] = dlg
  444. dlg.Show()
  445. def _export(self, exportInfo, decorations):
  446. size = self.frame.animationPanel.GetSize()
  447. if self.temporalMode == TemporalMode.TEMPORAL:
  448. timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
  449. frameCount = len(timeLabels)
  450. else:
  451. frameCount = self.animationData[
  452. 0].mapCount # should be the same for all
  453. animWinSize = []
  454. animWinPos = []
  455. animWinIndex = []
  456. legends = [anim.legendCmd for anim in self.animationData]
  457. # determine position and sizes of bitmaps
  458. for i, (win, anim) in enumerate(zip(self.mapwindows, self.animations)):
  459. if anim.IsActive():
  460. pos = win.GetPosition()
  461. animWinPos.append(pos)
  462. animWinSize.append(win.GetSize())
  463. animWinIndex.append(i)
  464. images = []
  465. busy = wx.BusyInfo(
  466. _("Preparing export, please wait..."),
  467. parent=self.frame)
  468. wx.GetApp().Yield()
  469. lastBitmaps = {}
  470. fgcolor = UserSettings.Get(
  471. group='animation',
  472. key='font',
  473. subkey='fgcolor')
  474. bgcolor = UserSettings.Get(
  475. group='animation',
  476. key='font',
  477. subkey='bgcolor')
  478. for frameIndex in range(frameCount):
  479. image = EmptyImage(*size)
  480. image.Replace(0, 0, 0, 255, 255, 255)
  481. # collect bitmaps of all windows and paste them into the one
  482. for i in animWinIndex:
  483. frameId = self.animations[i].GetFrame(frameIndex)
  484. if not UserSettings.Get(group='animation', key='temporal',
  485. subkey=['nodata', 'enable']):
  486. if frameId is not None:
  487. bitmap = self.bitmapProvider.GetBitmap(frameId)
  488. lastBitmaps[i] = bitmap
  489. else:
  490. if i not in lastBitmaps:
  491. lastBitmaps[i] = wx.NullBitmap()
  492. else:
  493. bitmap = self.bitmapProvider.GetBitmap(frameId)
  494. lastBitmaps[i] = bitmap
  495. im = ImageFromBitmap(lastBitmaps[i])
  496. # add legend if used
  497. legend = legends[i]
  498. if legend:
  499. legendBitmap = self.bitmapProvider.LoadOverlay(legend)
  500. x, y = self.mapwindows[i].GetOverlayPos()
  501. legImage = ImageFromBitmap(legendBitmap)
  502. # not so nice result, can we handle the transparency
  503. # otherwise?
  504. legImage.ConvertAlphaToMask()
  505. im.Paste(legImage, x, y)
  506. if im.GetSize() != animWinSize[i]:
  507. im.Rescale(*animWinSize[i])
  508. image.Paste(im, *animWinPos[i])
  509. # paste decorations
  510. for decoration in decorations:
  511. # add image
  512. x = decoration['pos'][0] / 100. * size[0]
  513. y = decoration['pos'][1] / 100. * size[1]
  514. if decoration['name'] == 'image':
  515. decImage = wx.Image(decoration['file'])
  516. elif decoration['name'] == 'time':
  517. timeLabel = timeLabels[frameIndex]
  518. if timeLabel[1]: # interval
  519. text = _("%(from)s %(dash)s %(to)s") % {
  520. 'from': timeLabel[0],
  521. 'dash': u"\u2013", 'to': timeLabel[1]}
  522. else:
  523. if self.temporalManager.GetTemporalType() == TemporalType.ABSOLUTE:
  524. text = timeLabel[0]
  525. else:
  526. text = _("%(start)s %(unit)s") % \
  527. {'start': timeLabel[0], 'unit': timeLabel[2]}
  528. decImage = RenderText(
  529. text, decoration['font'],
  530. bgcolor, fgcolor).ConvertToImage()
  531. elif decoration['name'] == 'text':
  532. text = decoration['text']
  533. decImage = RenderText(
  534. text, decoration['font'],
  535. bgcolor, fgcolor).ConvertToImage()
  536. image.Paste(decImage, x, y)
  537. images.append(image)
  538. del busy
  539. # export
  540. pilImages = [WxImageToPil(image) for image in images]
  541. self.busy = wx.BusyInfo(_("Exporting animation, please wait..."),
  542. parent=self.frame)
  543. wx.GetApp().Yield()
  544. try:
  545. def export_avi_callback(event):
  546. error = event.ret
  547. del self.busy
  548. if error:
  549. GError(parent=self.frame, message=error)
  550. return
  551. if exportInfo['method'] == 'sequence':
  552. filename = os.path.join(
  553. exportInfo['directory'],
  554. exportInfo['prefix'] +
  555. '.' +
  556. exportInfo['format'].lower())
  557. writeIms(filename=filename, images=pilImages)
  558. elif exportInfo['method'] == 'gif':
  559. writeGif(filename=exportInfo['file'], images=pilImages,
  560. duration=self.timeTick / float(1000), repeat=True)
  561. elif exportInfo['method'] == 'swf':
  562. writeSwf(filename=exportInfo['file'], images=pilImages,
  563. duration=self.timeTick / float(1000), repeat=True)
  564. elif exportInfo['method'] == 'avi':
  565. thread = gThread()
  566. thread.Run(callable=writeAvi,
  567. filename=exportInfo['file'],
  568. images=pilImages,
  569. duration=self.timeTick / float(1000),
  570. encoding=exportInfo['encoding'],
  571. inputOptions=exportInfo['options'],
  572. bg_task=True,
  573. ondone=export_avi_callback,
  574. )
  575. except Exception as e:
  576. del self.busy
  577. GError(parent=self.frame, message=str(e))
  578. return
  579. if exportInfo['method'] in ('sequence', 'gif', 'swf'):
  580. del self.busy