controller.py 24 KB

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