controller.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  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 %d.") %
  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. regions = anim.GetRegions()
  298. self.animations[i].SetFrames(
  299. [HashCmds(cmdList, region) for cmdList,
  300. region in zip(anim.cmdMatrix, regions)])
  301. self.animations[i].SetActive(True)
  302. else:
  303. for i in range(len(self.animations)):
  304. if i not in activeIndices:
  305. self.animations[i].SetActive(False)
  306. continue
  307. anim = [anim for anim in self.animationData
  308. if anim.windowIndex == i][0]
  309. regions = anim.GetRegions()
  310. identifiers = sampleCmdMatrixAndCreateNames(
  311. anim.cmdMatrix, mapNamesDict[
  312. anim.firstStdsNameType[0]], regions)
  313. self.animations[i].SetFrames(identifiers)
  314. self.animations[i].SetActive(True)
  315. def _updateWindows(self, activeIndices):
  316. # add or remove window
  317. for windowIndex in range(len(self.animations)):
  318. if not self.frame.IsWindowShown(
  319. windowIndex) and windowIndex in activeIndices:
  320. self.frame.AddWindow(windowIndex)
  321. elif self.frame.IsWindowShown(windowIndex) and windowIndex not in activeIndices:
  322. self.frame.RemoveWindow(windowIndex)
  323. def _updateBitmapData(self):
  324. # unload previous data
  325. self.bitmapProvider.Unload()
  326. # load new data
  327. for animData in self.animationData:
  328. if animData.viewMode == '2d':
  329. self._set2DData(animData)
  330. else:
  331. self._load3DData(animData)
  332. self._loadLegend(animData)
  333. color = UserSettings.Get(
  334. group='animation',
  335. key='bgcolor',
  336. subkey='color')
  337. cpus = UserSettings.Get(
  338. group='animation',
  339. key='nprocs',
  340. subkey='value')
  341. self.bitmapProvider.Load(nprocs=cpus, bgcolor=color)
  342. # clear pools
  343. self.bitmapPool.Clear()
  344. self.mapFilesPool.Clear()
  345. def _set2DData(self, animationData):
  346. opacities = [
  347. layer.opacity for layer in animationData.layerList
  348. if layer.active]
  349. #w, h = self.mapwindows[animationData.GetWindowIndex()].GetClientSize()
  350. regions = animationData.GetRegions()
  351. self.bitmapProvider.SetCmds(
  352. animationData.cmdMatrix, opacities, regions)
  353. def _load3DData(self, animationData):
  354. nviz = animationData.GetNvizCommands()
  355. self.bitmapProvider.SetCmds3D(nviz['commands'], nviz['region'])
  356. def _loadLegend(self, animationData):
  357. if animationData.legendCmd:
  358. try:
  359. bitmap = self.bitmapProvider.LoadOverlay(
  360. animationData.legendCmd)
  361. try:
  362. from PIL import Image
  363. for param in animationData.legendCmd:
  364. if param.startswith('at'):
  365. b, t, l, r = param.split('=')[1].split(',')
  366. x, y = float(l) / 100., 1 - float(t) / 100.
  367. break
  368. except ImportError:
  369. x, y = 0, 0
  370. self.mapwindows[
  371. animationData.windowIndex].SetOverlay(
  372. bitmap, x, y)
  373. except GException:
  374. GError(message=_("Failed to display legend."))
  375. else:
  376. self.mapwindows[animationData.windowIndex].ClearOverlay()
  377. def EvaluateInput(self, animationData):
  378. stds = 0
  379. maps = 0
  380. mapCount = set()
  381. tempManager = None
  382. windowIndex = []
  383. for anim in animationData:
  384. for layer in anim.layerList:
  385. if layer.active and hasattr(layer, 'maps'):
  386. if layer.mapType in ('strds', 'stvds', 'str3ds'):
  387. stds += 1
  388. else:
  389. maps += 1
  390. mapCount.add(len(layer.maps))
  391. windowIndex.append(anim.windowIndex)
  392. if maps and stds:
  393. temporalMode = TemporalMode.NONTEMPORAL
  394. elif maps:
  395. temporalMode = TemporalMode.NONTEMPORAL
  396. elif stds:
  397. temporalMode = TemporalMode.TEMPORAL
  398. else:
  399. temporalMode = None
  400. if temporalMode == TemporalMode.NONTEMPORAL:
  401. if len(mapCount) > 1:
  402. raise GException(
  403. _("Inconsistent number of maps, please check input data."))
  404. elif temporalMode == TemporalMode.TEMPORAL:
  405. tempManager = TemporalManager()
  406. # these raise GException:
  407. for anim in animationData:
  408. tempManager.AddTimeSeries(*anim.firstStdsNameType)
  409. message = tempManager.EvaluateInputData()
  410. if message:
  411. GMessage(parent=self.frame, message=message)
  412. return temporalMode, tempManager
  413. def Reload(self):
  414. self.EndAnimation()
  415. color = UserSettings.Get(
  416. group='animation',
  417. key='bgcolor',
  418. subkey='color')
  419. cpus = UserSettings.Get(
  420. group='animation',
  421. key='nprocs',
  422. subkey='value')
  423. self.bitmapProvider.Load(nprocs=cpus, bgcolor=color, force=True)
  424. self.EndAnimation()
  425. def Export(self):
  426. if not self.animationData:
  427. GMessage(parent=self.frame, message=_("No animation to export."))
  428. return
  429. if 'export' in self._dialogs:
  430. self._dialogs['export'].Show()
  431. self._dialogs['export'].Raise()
  432. else:
  433. dlg = ExportDialog(self.frame, temporal=self.temporalMode,
  434. timeTick=self.timeTick)
  435. dlg.CenterOnParent()
  436. dlg.doExport.connect(self._export)
  437. self._dialogs['export'] = dlg
  438. dlg.Show()
  439. def _export(self, exportInfo, decorations):
  440. size = self.frame.animationPanel.GetSize()
  441. if self.temporalMode == TemporalMode.TEMPORAL:
  442. timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
  443. frameCount = len(timeLabels)
  444. else:
  445. frameCount = self.animationData[
  446. 0].mapCount # should be the same for all
  447. animWinSize = []
  448. animWinPos = []
  449. animWinIndex = []
  450. legends = [anim.legendCmd for anim in self.animationData]
  451. # determine position and sizes of bitmaps
  452. for i, (win, anim) in enumerate(zip(self.mapwindows, self.animations)):
  453. if anim.IsActive():
  454. pos = win.GetPosition()
  455. animWinPos.append(pos)
  456. animWinSize.append(win.GetSize())
  457. animWinIndex.append(i)
  458. images = []
  459. busy = wx.BusyInfo(
  460. _("Preparing export, please wait..."),
  461. parent=self.frame)
  462. wx.GetApp().Yield()
  463. lastBitmaps = {}
  464. fgcolor = UserSettings.Get(
  465. group='animation',
  466. key='font',
  467. subkey='fgcolor')
  468. bgcolor = UserSettings.Get(
  469. group='animation',
  470. key='font',
  471. subkey='bgcolor')
  472. for frameIndex in range(frameCount):
  473. image = EmptyImage(*size)
  474. image.Replace(0, 0, 0, 255, 255, 255)
  475. # collect bitmaps of all windows and paste them into the one
  476. for i in animWinIndex:
  477. frameId = self.animations[i].GetFrame(frameIndex)
  478. if not UserSettings.Get(group='animation', key='temporal',
  479. subkey=['nodata', 'enable']):
  480. if frameId is not None:
  481. bitmap = self.bitmapProvider.GetBitmap(frameId)
  482. lastBitmaps[i] = bitmap
  483. else:
  484. if i not in lastBitmaps:
  485. lastBitmaps[i] = wx.NullBitmap()
  486. else:
  487. bitmap = self.bitmapProvider.GetBitmap(frameId)
  488. lastBitmaps[i] = bitmap
  489. im = ImageFromBitmap(lastBitmaps[i])
  490. # add legend if used
  491. legend = legends[i]
  492. if legend:
  493. legendBitmap = self.bitmapProvider.LoadOverlay(legend)
  494. x, y = self.mapwindows[i].GetOverlayPos()
  495. legImage = ImageFromBitmap(legendBitmap)
  496. # not so nice result, can we handle the transparency
  497. # otherwise?
  498. legImage.ConvertAlphaToMask()
  499. im.Paste(legImage, x, y)
  500. if im.GetSize() != animWinSize[i]:
  501. im.Rescale(*animWinSize[i])
  502. image.Paste(im, *animWinPos[i])
  503. # paste decorations
  504. for decoration in decorations:
  505. # add image
  506. x = decoration['pos'][0] / 100. * size[0]
  507. y = decoration['pos'][1] / 100. * size[1]
  508. if decoration['name'] == 'image':
  509. decImage = wx.Image(decoration['file'])
  510. elif decoration['name'] == 'time':
  511. timeLabel = timeLabels[frameIndex]
  512. if timeLabel[1]: # interval
  513. text = _("%(from)s %(dash)s %(to)s") % {
  514. 'from': timeLabel[0],
  515. 'dash': u"\u2013", 'to': timeLabel[1]}
  516. else:
  517. if self.temporalManager.GetTemporalType() == TemporalType.ABSOLUTE:
  518. text = timeLabel[0]
  519. else:
  520. text = _("%(start)s %(unit)s") % \
  521. {'start': timeLabel[0], 'unit': timeLabel[2]}
  522. decImage = RenderText(
  523. text, decoration['font'],
  524. bgcolor, fgcolor).ConvertToImage()
  525. elif decoration['name'] == 'text':
  526. text = decoration['text']
  527. decImage = RenderText(
  528. text, decoration['font'],
  529. bgcolor, fgcolor).ConvertToImage()
  530. image.Paste(decImage, x, y)
  531. images.append(image)
  532. del busy
  533. # export
  534. pilImages = [WxImageToPil(image) for image in images]
  535. self.busy = wx.BusyInfo(_("Exporting animation, please wait..."),
  536. parent=self.frame)
  537. wx.GetApp().Yield()
  538. try:
  539. def export_avi_callback(event):
  540. error = event.ret
  541. del self.busy
  542. if error:
  543. GError(parent=self.frame, message=error)
  544. return
  545. if exportInfo['method'] == 'sequence':
  546. filename = os.path.join(
  547. exportInfo['directory'],
  548. exportInfo['prefix'] +
  549. '.' +
  550. exportInfo['format'].lower())
  551. writeIms(filename=filename, images=pilImages)
  552. elif exportInfo['method'] == 'gif':
  553. writeGif(filename=exportInfo['file'], images=pilImages,
  554. duration=self.timeTick / float(1000), repeat=True)
  555. elif exportInfo['method'] == 'swf':
  556. writeSwf(filename=exportInfo['file'], images=pilImages,
  557. duration=self.timeTick / float(1000), repeat=True)
  558. elif exportInfo['method'] == 'avi':
  559. thread = gThread()
  560. thread.Run(callable=writeAvi,
  561. filename=exportInfo['file'],
  562. images=pilImages,
  563. duration=self.timeTick / float(1000),
  564. encoding=exportInfo['encoding'],
  565. inputOptions=exportInfo['options'],
  566. bg_task=True,
  567. ondone=export_avi_callback,
  568. )
  569. except Exception as e:
  570. del self.busy
  571. GError(parent=self.frame, message=str(e))
  572. return
  573. if exportInfo['method'] in ('sequence', 'gif', 'swf'):
  574. del self.busy