import logging
import os

import ctk
import qt
import vtk

import slicer
from slicer.i18n import tr as _
from slicer.i18n import translate
from slicer.ScriptedLoadableModule import *


#
# ScreenCapture
#


class ScreenCapture(ScriptedLoadableModule):
    """Uses ScriptedLoadableModule base class, available at:
    https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py
    """

    def __init__(self, parent):
        ScriptedLoadableModule.__init__(self, parent)
        self.parent.title = _("Screen Capture")
        self.parent.categories = [translate("qSlicerAbstractCoreModule", "Utilities")]
        self.parent.dependencies = []
        self.parent.contributors = ["Andras Lasso (PerkLab Queen's University)"]
        self.parent.helpText = _("""
This module captures image sequences and videos
from dynamic contents shown in 3D and slice viewers.
""")
        self.parent.helpText += self.getDefaultModuleDocumentationLink()
        self.parent.acknowledgementText = _("""
This work was was funded by Cancer Care Ontario
and the Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO)
""")


#
# ScreenCaptureWidget
#

VIEW_SLICE = "slice"
VIEW_3D = "3d"

AXIS_YAW = 0
AXIS_PITCH = 1


class ScreenCaptureWidget(ScriptedLoadableModuleWidget):
    """Uses ScriptedLoadableModuleWidget base class, available at:
    https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py
    """

    def setup(self):
        ScriptedLoadableModuleWidget.setup(self)

        self.logic = ScreenCaptureLogic()
        self.logic.logCallback = self.addLog
        self.viewNodeType = None
        self.animationMode = None
        self.createdOutputFile = None

        self.snapshotIndex = 0  # this counter is used for determining file names for single-image snapshots
        self.snapshotOutputDir = None
        self.snapshotFileNamePattern = None

        # Instantiate and connect widgets ...

        #
        # Input area
        #
        self.inputCollapsibleButton = ctk.ctkCollapsibleButton()
        self.inputCollapsibleButton.text = _("Input")
        self.layout.addWidget(self.inputCollapsibleButton)
        inputFormLayout = qt.QFormLayout(self.inputCollapsibleButton)

        # Input view selector
        self.viewNodeSelector = slicer.qMRMLNodeComboBox()
        self.viewNodeSelector.nodeTypes = ["vtkMRMLSliceNode", "vtkMRMLViewNode"]
        self.viewNodeSelector.addEnabled = False
        self.viewNodeSelector.removeEnabled = False
        self.viewNodeSelector.noneEnabled = False
        self.viewNodeSelector.showHidden = False
        self.viewNodeSelector.showChildNodeTypes = False
        self.viewNodeSelector.setMRMLScene(slicer.mrmlScene)
        self.viewNodeSelector.setToolTip(_("This slice or 3D view will be updated during capture."
                                         "Only this view will be captured unless 'Capture all views' option is enabled."))
        inputFormLayout.addRow(_("Main view: "), self.viewNodeSelector)

        self.captureAllViewsCheckBox = qt.QCheckBox(" ")
        self.captureAllViewsCheckBox.checked = False
        self.captureAllViewsCheckBox.setToolTip(_("If checked, all views will be captured. If unchecked then only the selected view will be captured."))
        inputFormLayout.addRow(_("Capture all views:"), self.captureAllViewsCheckBox)

        # Mode
        self.animationModeWidget = qt.QComboBox()
        self.animationModeWidget.setToolTip(_("Specify how the main view will be modified during capture."))
        inputFormLayout.addRow(_("Capture mode:"), self.animationModeWidget)

        # Slice start offset position
        self.sliceStartOffsetLabel = qt.QLabel(_("Start sweep offset:"))
        self.sliceStartOffsetSliderWidget = ctk.ctkSliderWidget()
        self.sliceStartOffsetSliderWidget.singleStep = 30
        self.sliceStartOffsetSliderWidget.minimum = -100
        self.sliceStartOffsetSliderWidget.maximum = 100
        self.sliceStartOffsetSliderWidget.value = 0
        self.sliceStartOffsetSliderWidget.setToolTip(_("Start slice sweep offset."))
        inputFormLayout.addRow(self.sliceStartOffsetLabel, self.sliceStartOffsetSliderWidget)

        # Slice end offset position
        self.sliceEndOffsetLabel = qt.QLabel(_("End sweep offset:"))
        self.sliceEndOffsetSliderWidget = ctk.ctkSliderWidget()
        self.sliceEndOffsetSliderWidget.singleStep = 5
        self.sliceEndOffsetSliderWidget.minimum = -100
        self.sliceEndOffsetSliderWidget.maximum = 100
        self.sliceEndOffsetSliderWidget.value = 0
        self.sliceEndOffsetSliderWidget.setToolTip(_("End slice sweep offset."))
        inputFormLayout.addRow(self.sliceEndOffsetLabel, self.sliceEndOffsetSliderWidget)

        # 3D rotation range
        self.rotationLabel = qt.QLabel(_("Rotation range:"))
        self.rotationSliderWidget = ctk.ctkRangeWidget()
        self.rotationSliderWidget.singleStep = 5
        self.rotationSliderWidget.minimum = -180
        self.rotationSliderWidget.maximum = 180
        self.rotationSliderWidget.minimumValue = -180
        self.rotationSliderWidget.maximumValue = 180
        self.rotationSliderWidget.setToolTip(_("View rotation range, relative to current view orientation."))
        inputFormLayout.addRow(self.rotationLabel, self.rotationSliderWidget)

        # 3D rotation axis
        self.rotationAxisLabel = qt.QLabel(_("Rotation axis:"))
        self.rotationAxisWidget = ctk.ctkRangeWidget()
        self.rotationAxisWidget = qt.QComboBox()
        self.rotationAxisWidget.addItem(_("Yaw"), AXIS_YAW)
        self.rotationAxisWidget.addItem(_("Pitch"), AXIS_PITCH)
        inputFormLayout.addRow(self.rotationAxisLabel, self.rotationAxisWidget)

        # Sequence browser node selector
        self.sequenceBrowserNodeSelectorLabel = qt.QLabel(_("Sequence:"))
        self.sequenceBrowserNodeSelectorWidget = slicer.qMRMLNodeComboBox()
        self.sequenceBrowserNodeSelectorWidget.nodeTypes = ["vtkMRMLSequenceBrowserNode"]
        self.sequenceBrowserNodeSelectorWidget.addEnabled = False
        self.sequenceBrowserNodeSelectorWidget.removeEnabled = False
        self.sequenceBrowserNodeSelectorWidget.noneEnabled = False
        self.sequenceBrowserNodeSelectorWidget.showHidden = False
        self.sequenceBrowserNodeSelectorWidget.setMRMLScene(slicer.mrmlScene)
        self.sequenceBrowserNodeSelectorWidget.setToolTip(_("Items defined by this sequence browser will be replayed."))
        inputFormLayout.addRow(self.sequenceBrowserNodeSelectorLabel, self.sequenceBrowserNodeSelectorWidget)

        # Sequence start index
        self.sequenceStartItemIndexLabel = qt.QLabel(_("Start index:"))
        self.sequenceStartItemIndexWidget = ctk.ctkSliderWidget()
        self.sequenceStartItemIndexWidget.minimum = 0
        self.sequenceStartItemIndexWidget.decimals = 0
        self.sequenceStartItemIndexWidget.setToolTip(_("First item in the sequence to capture."))
        inputFormLayout.addRow(self.sequenceStartItemIndexLabel, self.sequenceStartItemIndexWidget)

        # Sequence end index
        self.sequenceEndItemIndexLabel = qt.QLabel(_("End index:"))
        self.sequenceEndItemIndexWidget = ctk.ctkSliderWidget()
        self.sequenceEndItemIndexWidget.minimum = 0
        self.sequenceEndItemIndexWidget.decimals = 0
        self.sequenceEndItemIndexWidget.setToolTip(_("Last item in the sequence to capture."))
        inputFormLayout.addRow(self.sequenceEndItemIndexLabel, self.sequenceEndItemIndexWidget)

        #
        # Output area
        #
        self.outputCollapsibleButton = ctk.ctkCollapsibleButton()
        self.outputCollapsibleButton.text = _("Output")
        self.layout.addWidget(self.outputCollapsibleButton)
        outputFormLayout = qt.QFormLayout(self.outputCollapsibleButton)

        self.outputTypeLabel = qt.QLabel(_("Output type:"))
        self.outputTypeWidget = qt.QComboBox()
        self.outputTypeWidget.setToolTip(
            _("Select how captured images will be saved. Video mode requires setting of ffmpeg executable path in Advanced section."))
        self.outputTypeWidget.addItem(_("image series"), "IMAGE_SERIES")
        self.outputTypeWidget.addItem(_("video"), "VIDEO")
        self.outputTypeWidget.addItem(_("lightbox image"), "LIGHTBOX_IMAGE")
        outputFormLayout.addRow(self.outputTypeLabel, self.outputTypeWidget)

        # Number of steps value

        self.numberOfStepsLabel = qt.QLabel(_("Number of images:"))

        self.numberOfStepsSliderWidget = ctk.ctkSliderWidget()
        self.numberOfStepsSliderWidget.singleStep = 1
        self.numberOfStepsSliderWidget.pageStep = 10
        self.numberOfStepsSliderWidget.minimum = 1
        self.numberOfStepsSliderWidget.maximum = 600
        self.numberOfStepsSliderWidget.value = 31
        self.numberOfStepsSliderWidget.decimals = 0
        self.numberOfStepsSliderWidget.setToolTip(_("Number of images extracted between start and stop positions."))

        outputFormLayout.addRow(self.numberOfStepsLabel, self.numberOfStepsSliderWidget)

        # Output directory selector
        self.outputDirSelector = ctk.ctkPathLineEdit()
        self.outputDirSelector.filters = ctk.ctkPathLineEdit.Dirs
        self.outputDirSelector.settingKey = "ScreenCaptureOutputDir"
        outputFormLayout.addRow(_("Output directory:"), self.outputDirSelector)
        if not self.outputDirSelector.currentPath:
            defaultOutputPath = os.path.abspath(os.path.join(slicer.app.defaultScenePath, "SlicerCapture"))
            self.outputDirSelector.setCurrentPath(defaultOutputPath)

        self.fileNamePatternWidget = qt.QLineEdit()
        self.fileNamePatternWidget.setToolTip(
            _("String that defines file name, type, and numbering scheme. Default: image%05d.png."))
        self.fileNamePatternWidget.text = _("image_%05d.png")

        self.videoFileNameWidget = qt.QLineEdit()
        self.videoFileNameWidget.setToolTip(_("String that defines file name and type."))
        self.videoFileNameWidget.text = _("SlicerCapture.mp4")

        self.lightboxImageFileNameWidget = qt.QLineEdit()
        self.lightboxImageFileNameWidget.setToolTip(_("String that defines output lightbox file name and type."))
        self.lightboxImageFileNameWidget.text = _("SlicerCaptureLightbox.png")

        hbox = qt.QHBoxLayout()
        hbox.addWidget(self.fileNamePatternWidget)
        hbox.addWidget(self.videoFileNameWidget)
        hbox.addWidget(self.lightboxImageFileNameWidget)
        outputFormLayout.addRow(_("Output file name:"), hbox)

        self.videoFormatLabel = qt.QLabel(_("Video format:"))

        self.videoFormatWidget = qt.QComboBox()
        self.videoFormatWidget.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)
        for videoFormatPreset in self.logic.videoFormatPresets:
            self.videoFormatWidget.addItem(videoFormatPreset["name"])
        outputFormLayout.addRow(self.videoFormatLabel, self.videoFormatWidget)

        self.videoLengthLabel = qt.QLabel(_("Video length:"))

        self.videoLengthSliderWidget = ctk.ctkSliderWidget()
        self.videoLengthSliderWidget.singleStep = 0.1
        self.videoLengthSliderWidget.minimum = 0.1
        self.videoLengthSliderWidget.maximum = 30
        self.videoLengthSliderWidget.value = 5
        self.videoLengthSliderWidget.suffix = "s"
        self.videoLengthSliderWidget.decimals = 1
        self.videoLengthSliderWidget.setToolTip(_("Length of the exported video in seconds (without backward steps and repeating)."))
        outputFormLayout.addRow(self.videoLengthLabel, self.videoLengthSliderWidget)

        self.videoFrameRateLabel = qt.QLabel(_("Video frame rate:"))

        self.videoFrameRateSliderWidget = ctk.ctkSliderWidget()
        self.videoFrameRateSliderWidget.singleStep = 0.1
        self.videoFrameRateSliderWidget.minimum = 0.1
        self.videoFrameRateSliderWidget.maximum = 60
        self.videoFrameRateSliderWidget.value = 5.0
        self.videoFrameRateSliderWidget.suffix = "fps"
        self.videoFrameRateSliderWidget.decimals = 3
        self.videoFrameRateSliderWidget.setToolTip(_("Frame rate in frames per second."))
        outputFormLayout.addRow(self.videoFrameRateLabel, self.videoFrameRateSliderWidget)

        #
        # Advanced area
        #
        self.advancedCollapsibleButton = ctk.ctkCollapsibleButton()
        self.advancedCollapsibleButton.text = _("Advanced")
        self.advancedCollapsibleButton.collapsed = True
        outputFormLayout.addRow(self.advancedCollapsibleButton)
        advancedFormLayout = qt.QFormLayout(self.advancedCollapsibleButton)

        self.forwardBackwardCheckBox = qt.QCheckBox(" ")
        self.forwardBackwardCheckBox.checked = False
        self.forwardBackwardCheckBox.setToolTip(_("If checked, image series will be generated playing forward and then backward."))
        advancedFormLayout.addRow(_("Forward-backward:"), self.forwardBackwardCheckBox)

        self.repeatSliderWidget = ctk.ctkSliderWidget()
        self.repeatSliderWidget.decimals = 0
        self.repeatSliderWidget.singleStep = 1
        self.repeatSliderWidget.minimum = 1
        self.repeatSliderWidget.maximum = 50
        self.repeatSliderWidget.value = 1
        self.repeatSliderWidget.setToolTip(_("Number of times image series are repeated. Useful for making short videos longer for playback in software"
                                             " that does not support looped playback."))
        advancedFormLayout.addRow(_("Repeat:"), self.repeatSliderWidget)

        ffmpegPath = self.logic.getFfmpegPath()
        self.ffmpegPathSelector = ctk.ctkPathLineEdit()
        self.ffmpegPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength
        self.ffmpegPathSelector.setCurrentPath(ffmpegPath)
        self.ffmpegPathSelector.nameFilters = [self.logic.getFfmpegExecutableFilename()]
        self.ffmpegPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
        self.ffmpegPathSelector.setToolTip(_("Set the path to ffmpeg executable. Download from: https://www.ffmpeg.org/"))
        advancedFormLayout.addRow(_("ffmpeg executable:"), self.ffmpegPathSelector)

        self.videoExportFfmpegWarning = qt.QLabel(_('<qt><b><font color="red">Set valid ffmpeg executable path! ' +
                                                    '<a href="https://slicer.readthedocs.io/en/latest/user_guide/modules/screencapture.html#setting-up-ffmpeg">'
                                                    'Help...</a></font></b></qt>'))
        self.videoExportFfmpegWarning.connect("linkActivated(QString)", self.openURL)
        self.videoExportFfmpegWarning.setVisible(False)
        advancedFormLayout.addRow("", self.videoExportFfmpegWarning)

        self.extraVideoOptionsWidget = qt.QLineEdit()
        self.extraVideoOptionsWidget.setToolTip(_("Additional video conversion options passed to ffmpeg. Parameters -i (input files), -y"
                                                  "(overwrite without asking), -r (frame rate), -start_number are specified by the module and therefore"
                                                  "should not be included in this list."))
        advancedFormLayout.addRow(_("Video extra options:"), self.extraVideoOptionsWidget)

        self.lightboxColumnCountSliderWidget = ctk.ctkSliderWidget()
        self.lightboxColumnCountSliderWidget.decimals = 0
        self.lightboxColumnCountSliderWidget.singleStep = 1
        self.lightboxColumnCountSliderWidget.minimum = 1
        self.lightboxColumnCountSliderWidget.maximum = 20
        self.lightboxColumnCountSliderWidget.value = 6
        self.lightboxColumnCountSliderWidget.setToolTip(_("Number of columns in lightbox image"))
        advancedFormLayout.addRow(_("Lightbox image columns:"), self.lightboxColumnCountSliderWidget)

        self.maxFramesWidget = qt.QSpinBox()
        self.maxFramesWidget.setRange(1, 9999)
        self.maxFramesWidget.setValue(600)
        self.maxFramesWidget.setToolTip(
            _("Maximum number of images to be captured (without backward steps and repeating)."))
        advancedFormLayout.addRow(_("Maximum number of images:"), self.maxFramesWidget)

        self.volumeNodeComboBox = slicer.qMRMLNodeComboBox()
        self.volumeNodeComboBox.nodeTypes = ["vtkMRMLVectorVolumeNode"]
        self.volumeNodeComboBox.baseName = _("Screenshot")
        self.volumeNodeComboBox.renameEnabled = True
        self.volumeNodeComboBox.noneEnabled = True
        self.volumeNodeComboBox.setToolTip(
            _("Select a volume node to store the captured image in the scene"
              " instead of just writing immediately to disk. Requires output 'Number of images' to be set to 1."))
        self.volumeNodeComboBox.setMRMLScene(slicer.mrmlScene)
        advancedFormLayout.addRow(_("Output volume node:"), self.volumeNodeComboBox)

        self.showViewControllersCheckBox = qt.QCheckBox(" ")
        self.showViewControllersCheckBox.checked = False
        self.showViewControllersCheckBox.setToolTip(_("If checked, images will be captured with view controllers visible."))
        advancedFormLayout.addRow(_("View controllers:"), self.showViewControllersCheckBox)

        self.transparentBackgroundCheckBox = qt.QCheckBox(" ")
        self.transparentBackgroundCheckBox.checked = False
        self.transparentBackgroundCheckBox.setToolTip(_("If checked, images will be captured with transparent background."))
        advancedFormLayout.addRow(_("Transparent background:"), self.transparentBackgroundCheckBox)

        watermarkEnabled = slicer.util.settingsValue("ScreenCapture/WatermarkEnabled", False, converter=slicer.util.toBool)

        self.watermarkEnabledCheckBox = qt.QCheckBox(" ")
        self.watermarkEnabledCheckBox.checked = watermarkEnabled
        self.watermarkEnabledCheckBox.setToolTip(_("If checked, selected watermark image will be added to all exported images."))

        self.watermarkPositionWidget = qt.QComboBox()
        self.watermarkPositionWidget.enabled = watermarkEnabled
        self.watermarkPositionWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
        self.watermarkPositionWidget.setToolTip(_("Add a watermark image to all exported images."))
        for watermarkPositionPreset in self.logic.watermarkPositionPresets:
            self.watermarkPositionWidget.addItem(watermarkPositionPreset["name"])
        self.watermarkPositionWidget.setCurrentText(
            slicer.util.settingsValue("ScreenCapture/WatermarkPosition", self.logic.watermarkPositionPresets[0]["name"]))

        self.watermarkSizeSliderWidget = qt.QSpinBox()
        self.watermarkSizeSliderWidget.enabled = watermarkEnabled
        self.watermarkSizeSliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
        self.watermarkSizeSliderWidget.singleStep = 10
        self.watermarkSizeSliderWidget.minimum = 10
        self.watermarkSizeSliderWidget.maximum = 1000
        self.watermarkSizeSliderWidget.value = 100
        self.watermarkSizeSliderWidget.suffix = "%"
        self.watermarkSizeSliderWidget.setToolTip(_("Size scaling applied to the watermark image. 100% is original size"))
        try:
            self.watermarkSizeSliderWidget.value = int(slicer.util.settingsValue("ScreenCapture/WatermarkSize", 100))
        except:
            pass

        self.watermarkOpacitySliderWidget = qt.QSpinBox()
        self.watermarkOpacitySliderWidget.enabled = watermarkEnabled
        self.watermarkOpacitySliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
        self.watermarkOpacitySliderWidget.singleStep = 10
        self.watermarkOpacitySliderWidget.minimum = 0
        self.watermarkOpacitySliderWidget.maximum = 100
        self.watermarkOpacitySliderWidget.value = 30
        self.watermarkOpacitySliderWidget.suffix = "%"
        self.watermarkOpacitySliderWidget.setToolTip(_("Opacity of the watermark image. 100% is fully opaque."))
        try:
            self.watermarkOpacitySliderWidget.value = int(slicer.util.settingsValue("ScreenCapture/WatermarkOpacity", 30))
        except:
            pass

        self.watermarkPathSelector = ctk.ctkPathLineEdit()
        self.watermarkPathSelector.enabled = watermarkEnabled
        self.watermarkPathSelector.settingKey = "ScreenCaptureWatermarkImagePath"
        self.watermarkPathSelector.nameFilters = ["*.png"]
        self.watermarkPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength
        self.watermarkPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
        self.watermarkPathSelector.setToolTip(_("Watermark image file in png format"))

        hbox = qt.QHBoxLayout()
        hbox.addWidget(self.watermarkEnabledCheckBox)
        hbox.addWidget(qt.QLabel(_("Position:")))
        hbox.addWidget(self.watermarkPositionWidget)
        hbox.addWidget(qt.QLabel(_("Size:")))
        hbox.addWidget(self.watermarkSizeSliderWidget)
        hbox.addWidget(qt.QLabel(_("Opacity:")))
        hbox.addWidget(self.watermarkOpacitySliderWidget)
        # hbox.addStretch()
        advancedFormLayout.addRow(_("Watermark image:"), hbox)

        hbox = qt.QHBoxLayout()
        hbox.addWidget(self.watermarkPathSelector)
        advancedFormLayout.addRow("", hbox)

        # Capture button
        self.captureButtonLabelCapture = _("Capture")
        self.captureButtonLabelCancel = _("Cancel")
        self.captureButton = qt.QPushButton(self.captureButtonLabelCapture)
        self.captureButton.toolTip = _("Capture slice sweep to image sequence.")
        self.showCreatedOutputFileButton = qt.QPushButton()
        self.showCreatedOutputFileButton.setIcon(qt.QIcon(":Icons/Go.png"))
        self.showCreatedOutputFileButton.setMaximumWidth(60)
        self.showCreatedOutputFileButton.enabled = False
        self.showCreatedOutputFileButton.toolTip = _("Show created output file.")
        hbox = qt.QHBoxLayout()
        hbox.addWidget(self.captureButton)
        hbox.addWidget(self.showCreatedOutputFileButton)
        self.layout.addLayout(hbox)

        self.statusLabel = qt.QPlainTextEdit()
        self.statusLabel.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
        self.statusLabel.setCenterOnScroll(True)
        self.layout.addWidget(self.statusLabel)

        #
        # Add vertical spacer
        # self.layout.addStretch(1)

        # connections
        self.captureButton.connect("clicked(bool)", self.onCaptureButton)
        self.showCreatedOutputFileButton.connect("clicked(bool)", self.onShowCreatedOutputFile)
        self.viewNodeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions)
        self.animationModeWidget.connect("currentIndexChanged(int)", self.updateViewOptions)
        self.sliceStartOffsetSliderWidget.connect("valueChanged(double)", self.setSliceOffset)
        self.sliceEndOffsetSliderWidget.connect("valueChanged(double)", self.setSliceOffset)
        self.sequenceBrowserNodeSelectorWidget.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions)
        self.sequenceStartItemIndexWidget.connect("valueChanged(double)", self.setSequenceItemIndex)
        self.sequenceEndItemIndexWidget.connect("valueChanged(double)", self.setSequenceItemIndex)
        self.outputTypeWidget.connect("currentIndexChanged(int)", self.updateOutputType)
        self.videoFormatWidget.connect("currentIndexChanged(int)", self.updateVideoFormat)
        self.maxFramesWidget.connect("valueChanged(int)", self.maxFramesChanged)
        self.videoLengthSliderWidget.connect("valueChanged(double)", self.setVideoLength)
        self.videoFrameRateSliderWidget.connect("valueChanged(double)", self.setVideoFrameRate)
        self.numberOfStepsSliderWidget.connect("valueChanged(double)", self.setNumberOfSteps)
        self.watermarkEnabledCheckBox.connect("toggled(bool)", self.watermarkPositionWidget, "setEnabled(bool)")
        self.watermarkEnabledCheckBox.connect("toggled(bool)", self.watermarkSizeSliderWidget, "setEnabled(bool)")
        self.watermarkEnabledCheckBox.connect("toggled(bool)", self.watermarkPathSelector, "setEnabled(bool)")

        self.setVideoLength()  # update frame rate based on video length
        self.updateOutputType()
        self.updateVideoFormat(0)
        self.updateViewOptions()

    def maxFramesChanged(self):
        self.numberOfStepsSliderWidget.maximum = self.maxFramesWidget.value

    def openURL(self, URL):
        qt.QDesktopServices().openUrl(qt.QUrl(URL))

    def onShowCreatedOutputFile(self):
        if not self.createdOutputFile:
            return
        qt.QDesktopServices().openUrl(qt.QUrl("file:///" + self.createdOutputFile, qt.QUrl.TolerantMode))

    def updateOutputType(self, selectionIndex=0):
        outputType = self.outputTypeWidget.currentData
        forceSingleImage = False
        if self.animationModeWidget.currentData == "SINGLE_FRAME":
            outputType = "IMAGE_SERIES"
            forceSingleImage = True

        self.numberOfStepsLabel.setVisible(not forceSingleImage)
        self.numberOfStepsSliderWidget.setVisible(not forceSingleImage)

        numberOfSteps = 1 if forceSingleImage else int(self.numberOfStepsSliderWidget.value)

        self.forwardBackwardCheckBox.enabled = numberOfSteps > 1
        self.repeatSliderWidget.enabled = numberOfSteps > 1
        self.volumeNodeComboBox.setEnabled(numberOfSteps == 1)

        self.outputTypeLabel.setVisible(not forceSingleImage)
        self.outputTypeWidget.setVisible(not forceSingleImage)

        self.videoFormatLabel.setVisible(outputType == "VIDEO")
        self.videoFormatWidget.setVisible(outputType == "VIDEO")
        self.videoLengthLabel.setVisible(outputType == "VIDEO")
        self.videoLengthSliderWidget.setVisible(outputType == "VIDEO")
        self.videoFrameRateLabel.setVisible(outputType == "VIDEO")
        self.videoFrameRateSliderWidget.setVisible(outputType == "VIDEO")
        self.fileNamePatternWidget.setVisible(outputType == "IMAGE_SERIES")
        self.videoFileNameWidget.setVisible(outputType == "VIDEO")
        self.lightboxImageFileNameWidget.setVisible(outputType == "LIGHTBOX_IMAGE")

    def updateVideoFormat(self, selectionIndex):
        videoFormatPreset = self.logic.videoFormatPresets[selectionIndex]

        import os

        filenameExt = os.path.splitext(self.videoFileNameWidget.text)
        self.videoFileNameWidget.text = filenameExt[0] + "." + videoFormatPreset["fileExtension"]

        self.extraVideoOptionsWidget.text = videoFormatPreset["extraVideoOptions"]

    def currentViewNodeType(self):
        viewNode = self.viewNodeSelector.currentNode()
        if not viewNode:
            return None
        elif viewNode.IsA("vtkMRMLSliceNode"):
            return VIEW_SLICE
        elif viewNode.IsA("vtkMRMLViewNode"):
            return VIEW_3D
        else:
            return None

    def addLog(self, text):
        """Append text to log window"""
        self.statusLabel.appendPlainText(text)
        self.statusLabel.ensureCursorVisible()
        slicer.app.processEvents()  # force update

    def cleanup(self):
        pass

    def updateViewOptions(self):
        sequencesModuleAvailable = hasattr(slicer.modules, "sequences")

        if self.viewNodeType != self.currentViewNodeType():
            self.viewNodeType = self.currentViewNodeType()

            self.animationModeWidget.clear()
            if self.viewNodeType == VIEW_SLICE:
                self.animationModeWidget.addItem(_("single frame"), "SINGLE_FRAME")
                self.animationModeWidget.addItem(_("slice sweep"), "SLICE_SWEEP")
                self.animationModeWidget.addItem(_("slice fade"), "SLICE_FADE")
            if self.viewNodeType == VIEW_3D:
                self.animationModeWidget.addItem(_("single frame"), "SINGLE_FRAME")
                self.animationModeWidget.addItem(_("3D rotation"), "3D_ROTATION")
            if sequencesModuleAvailable:
                self.animationModeWidget.addItem(_("sequence"), "SEQUENCE")

        self.animationMode = self.animationModeWidget.currentData

        # slice sweep
        self.sliceStartOffsetLabel.visible = self.animationMode == "SLICE_SWEEP"
        self.sliceStartOffsetSliderWidget.visible = self.animationMode == "SLICE_SWEEP"
        self.sliceEndOffsetLabel.visible = self.animationMode == "SLICE_SWEEP"
        self.sliceEndOffsetSliderWidget.visible = self.animationMode == "SLICE_SWEEP"
        if self.animationMode == "SLICE_SWEEP":
            offsetResolution = self.logic.getSliceOffsetResolution(self.viewNodeSelector.currentNode())
            sliceOffsetMin, sliceOffsetMax = self.logic.getSliceOffsetRange(self.viewNodeSelector.currentNode())

            wasBlocked = self.sliceStartOffsetSliderWidget.blockSignals(True)
            self.sliceStartOffsetSliderWidget.singleStep = offsetResolution
            self.sliceStartOffsetSliderWidget.minimum = sliceOffsetMin
            self.sliceStartOffsetSliderWidget.maximum = sliceOffsetMax
            self.sliceStartOffsetSliderWidget.value = sliceOffsetMin
            self.sliceStartOffsetSliderWidget.blockSignals(wasBlocked)

            wasBlocked = self.sliceEndOffsetSliderWidget.blockSignals(True)
            self.sliceEndOffsetSliderWidget.singleStep = offsetResolution
            self.sliceEndOffsetSliderWidget.minimum = sliceOffsetMin
            self.sliceEndOffsetSliderWidget.maximum = sliceOffsetMax
            self.sliceEndOffsetSliderWidget.value = sliceOffsetMax
            self.sliceEndOffsetSliderWidget.blockSignals(wasBlocked)

        # 3D rotation
        self.rotationLabel.visible = self.animationMode == "3D_ROTATION"
        self.rotationSliderWidget.visible = self.animationMode == "3D_ROTATION"
        self.rotationAxisLabel.visible = self.animationMode == "3D_ROTATION"
        self.rotationAxisWidget.visible = self.animationMode == "3D_ROTATION"

        # Sequence
        self.sequenceBrowserNodeSelectorLabel.visible = self.animationMode == "SEQUENCE"
        self.sequenceBrowserNodeSelectorWidget.visible = self.animationMode == "SEQUENCE"
        self.sequenceStartItemIndexLabel.visible = self.animationMode == "SEQUENCE"
        self.sequenceStartItemIndexWidget.visible = self.animationMode == "SEQUENCE"
        self.sequenceEndItemIndexLabel.visible = self.animationMode == "SEQUENCE"
        self.sequenceEndItemIndexWidget.visible = self.animationMode == "SEQUENCE"
        if self.animationMode == "SEQUENCE":
            sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode()

            sequenceItemCount = 0
            if sequenceBrowserNode and sequenceBrowserNode.GetMasterSequenceNode():
                sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes()

            if sequenceItemCount > 0:
                wasBlocked = self.sequenceStartItemIndexWidget.blockSignals(True)
                self.sequenceStartItemIndexWidget.maximum = sequenceItemCount - 1
                self.sequenceStartItemIndexWidget.value = 0
                self.sequenceStartItemIndexWidget.blockSignals(wasBlocked)

                wasBlocked = self.sequenceEndItemIndexWidget.blockSignals(True)
                self.sequenceEndItemIndexWidget.maximum = sequenceItemCount - 1
                self.sequenceEndItemIndexWidget.value = sequenceItemCount - 1
                self.sequenceEndItemIndexWidget.blockSignals(wasBlocked)

            self.sequenceStartItemIndexWidget.enabled = sequenceItemCount > 0
            self.sequenceEndItemIndexWidget.enabled = sequenceItemCount > 0

        self.updateOutputType()

    def setSliceOffset(self, offset):
        sliceLogic = self.logic.getSliceLogicFromSliceNode(self.viewNodeSelector.currentNode())
        sliceLogic.SetSliceOffset(offset)

    def setVideoLength(self, lengthSec=None):
        wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True)
        self.videoFrameRateSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoLengthSliderWidget.value
        self.videoFrameRateSliderWidget.blockSignals(wasBlocked)

    def setVideoFrameRate(self, frameRateFps):
        wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True)
        self.videoLengthSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoFrameRateSliderWidget.value
        self.videoFrameRateSliderWidget.blockSignals(wasBlocked)

    def setNumberOfSteps(self, steps):
        self.setVideoLength()
        self.updateViewOptions()

    def setForceSingleStep(self, force):
        self.updateViewOptions()

    def setSequenceItemIndex(self, index):
        sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode()
        sequenceBrowserNode.SetSelectedItemNumber(int(index))

    def enableInputOutputWidgets(self, enable):
        self.inputCollapsibleButton.setEnabled(enable)
        self.outputCollapsibleButton.setEnabled(enable)

    def onCaptureButton(self):
        # Disable capture button to prevent multiple clicks
        self.captureButton.setEnabled(False)
        self.enableInputOutputWidgets(False)
        slicer.app.processEvents()

        if self.captureButton.text == self.captureButtonLabelCancel:
            self.logic.requestCancel()
            return

        self.logic.setFfmpegPath(self.ffmpegPathSelector.currentPath)

        qt.QSettings().setValue("ScreenCapture/WatermarkEnabled", bool(self.watermarkEnabledCheckBox.checked))
        qt.QSettings().setValue("ScreenCapture/WatermarkPosition", self.watermarkPositionWidget.currentText)
        qt.QSettings().setValue("ScreenCapture/WatermarkOpacity", self.watermarkOpacitySliderWidget.value)
        qt.QSettings().setValue("ScreenCapture/WatermarkSize", self.watermarkSizeSliderWidget.value)

        if self.watermarkEnabledCheckBox.checked:
            if self.watermarkPathSelector.currentPath:
                self.logic.setWatermarkImagePath(self.watermarkPathSelector.currentPath)
                self.watermarkPathSelector.addCurrentPathToHistory()
            else:
                self.logic.setWatermarkImagePath(self.resourcePath("SlicerWatermark.png"))
            self.logic.setWatermarkPosition(self.watermarkPositionWidget.currentIndex)
            self.logic.setWatermarkSizePercent(self.watermarkSizeSliderWidget.value)
            self.logic.setWatermarkOpacityPercent(self.watermarkOpacitySliderWidget.value)
        else:
            self.logic.setWatermarkPosition(-1)

        self.statusLabel.plainText = ""

        videoOutputRequested = self.outputTypeWidget.currentData == "VIDEO"
        viewNode = self.viewNodeSelector.currentNode()
        numberOfSteps = int(self.numberOfStepsSliderWidget.value)
        if self.animationModeWidget.currentData == "SINGLE_FRAME":
            numberOfSteps = 1
        if numberOfSteps < 2:
            # If a single image is selected
            videoOutputRequested = False
        outputDir = self.outputDirSelector.currentPath
        self.outputDirSelector.addCurrentPathToHistory()

        self.videoExportFfmpegWarning.setVisible(False)
        if videoOutputRequested:
            if not self.logic.isFfmpegPathValid():
                # ffmpeg not found, try to automatically find it at common locations
                self.logic.findFfmpeg()
            if not self.logic.isFfmpegPathValid() and os.name == "nt":  # TODO: implement download for Linux/MacOS?
                # ffmpeg not found, offer downloading it
                if slicer.util.confirmOkCancelDisplay(
                    _("Video encoder not detected on your system. "
                      "Download ffmpeg video encoder?"),
                        windowTitle=_("Download confirmation")):
                    if not self.logic.ffmpegDownload():
                        slicer.util.errorDisplay(_("ffmpeg download failed"))
            if not self.logic.isFfmpegPathValid():
                # still not found, user has to specify path manually
                self.videoExportFfmpegWarning.setVisible(True)
                self.advancedCollapsibleButton.collapsed = False
                self.captureButton.setEnabled(True)
                self.enableInputOutputWidgets(True)
                return
            self.ffmpegPathSelector.currentPath = self.logic.getFfmpegPath()

        # Need to create a new random file pattern if video output is requested to make sure that new image files are not mixed up with
        # existing files in the output directory
        imageFileNamePattern = (self.fileNamePatternWidget.text
                                if (self.outputTypeWidget.currentData == "IMAGE_SERIES") else self.logic.getRandomFilePattern())

        self.captureButton.setEnabled(True)
        self.captureButton.text = self.captureButtonLabelCancel
        slicer.app.setOverrideCursor(qt.Qt.WaitCursor)
        captureAllViews = self.captureAllViewsCheckBox.checked
        transparentBackground = self.transparentBackgroundCheckBox.checked
        showViewControllers = self.showViewControllersCheckBox.checked
        if captureAllViews:
            self.logic.showViewControllers(showViewControllers)
        elif showViewControllers:
            logging.warning(_("View controllers are only available to be shown when capturing all views."))
        try:
            if numberOfSteps < 2:
                if imageFileNamePattern != self.snapshotFileNamePattern or outputDir != self.snapshotOutputDir:
                    self.snapshotIndex = 0
                if outputDir:
                    [filename, self.snapshotIndex] = self.logic.getNextAvailableFileName(outputDir, imageFileNamePattern, self.snapshotIndex)
                else:
                    filename = None
                view = None if captureAllViews else self.logic.viewFromNode(viewNode)
                volumeNode = None if numberOfSteps > 1 else self.volumeNodeComboBox.currentNode()
                self.logic.captureImageFromView(view, filename, transparentBackground, volumeNode=volumeNode)
                if filename:
                    self.logic.addLog(_("Write {filename}").format(filename=filename))
                if volumeNode:
                    self.logic.addLog(_("Write to volume node '{volumeName}'").format(volumeName=volumeNode.GetName()))
            elif self.animationModeWidget.currentData == "SLICE_SWEEP":
                self.logic.captureSliceSweep(viewNode, self.sliceStartOffsetSliderWidget.value,
                                             self.sliceEndOffsetSliderWidget.value, numberOfSteps, outputDir, imageFileNamePattern,
                                             captureAllViews=captureAllViews, transparentBackground=transparentBackground)
            elif self.animationModeWidget.currentData == "SLICE_FADE":
                self.logic.captureSliceFade(viewNode, numberOfSteps, outputDir, imageFileNamePattern,
                                            captureAllViews=captureAllViews, transparentBackground=transparentBackground)
            elif self.animationModeWidget.currentData == "3D_ROTATION":
                self.logic.capture3dViewRotation(viewNode, self.rotationSliderWidget.minimumValue,
                                                 self.rotationSliderWidget.maximumValue, numberOfSteps,
                                                 self.rotationAxisWidget.itemData(self.rotationAxisWidget.currentIndex),
                                                 outputDir, imageFileNamePattern,
                                                 captureAllViews=captureAllViews, transparentBackground=transparentBackground)
            elif self.animationModeWidget.currentData == "SEQUENCE":
                self.logic.captureSequence(viewNode, self.sequenceBrowserNodeSelectorWidget.currentNode(),
                                           self.sequenceStartItemIndexWidget.value, self.sequenceEndItemIndexWidget.value,
                                           numberOfSteps, outputDir, imageFileNamePattern,
                                           captureAllViews=captureAllViews, transparentBackground=transparentBackground)
            else:
                raise ValueError("Unsupported view node type.")

            import shutil

            fps = self.videoFrameRateSliderWidget.value

            if numberOfSteps > 1:
                forwardBackward = self.forwardBackwardCheckBox.checked
                numberOfRepeats = int(self.repeatSliderWidget.value)
                filePathPattern = os.path.join(outputDir, imageFileNamePattern)
                fileIndex = numberOfSteps
                for repeatIndex in range(numberOfRepeats):
                    if forwardBackward:
                        for step in reversed(range(1, numberOfSteps - 1)):
                            sourceFilename = filePathPattern % step
                            destinationFilename = filePathPattern % fileIndex
                            self.logic.addLog(_("Copy to {filename}").format(filename=destinationFilename))
                            shutil.copyfile(sourceFilename, destinationFilename)
                            fileIndex += 1
                    if repeatIndex < numberOfRepeats - 1:
                        for step in range(numberOfSteps):
                            sourceFilename = filePathPattern % step
                            destinationFilename = filePathPattern % fileIndex
                            self.logic.addLog(_("Copy to {filename}").format(filename=destinationFilename))
                            shutil.copyfile(sourceFilename, destinationFilename)
                            fileIndex += 1
                if forwardBackward and (numberOfSteps > 2):
                    numberOfSteps += numberOfSteps - 2
                numberOfSteps *= numberOfRepeats

            try:
                if videoOutputRequested:
                    self.logic.createVideo(fps, self.extraVideoOptionsWidget.text,
                                           outputDir, imageFileNamePattern, self.videoFileNameWidget.text)
                elif (self.outputTypeWidget.currentData == "LIGHTBOX_IMAGE"):
                    self.logic.createLightboxImage(int(self.lightboxColumnCountSliderWidget.value),
                                                   outputDir, imageFileNamePattern, numberOfSteps, self.lightboxImageFileNameWidget.text)
            finally:
                if not self.outputTypeWidget.currentData == "IMAGE_SERIES":
                    self.logic.deleteTemporaryFiles(outputDir, imageFileNamePattern, numberOfSteps)

            self.addLog(_("Done."))
            self.createdOutputFile = os.path.join(outputDir, self.videoFileNameWidget.text) if videoOutputRequested else outputDir
            self.showCreatedOutputFileButton.enabled = True
        except Exception as e:
            self.addLog(_("Error:") + str(e))

            import traceback

            traceback.print_exc()
            self.showCreatedOutputFileButton.enabled = False
            self.createdOutputFile = None
        if captureAllViews:
            self.logic.showViewControllers(True)
        slicer.app.restoreOverrideCursor()
        self.captureButton.text = self.captureButtonLabelCapture
        self.captureButton.setEnabled(True)
        self.enableInputOutputWidgets(True)


#
# ScreenCaptureLogic
#


class ScreenCaptureLogic(ScriptedLoadableModuleLogic):
    """This class should implement all the actual
    computation done by your module.  The interface
    should be such that other python code can import
    this class and make use of the functionality without
    requiring an instance of the Widget.
    Uses ScriptedLoadableModuleLogic base class, available at:
    https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py
    """

    def __init__(self):
        self.logCallback = None
        self.cancelRequested = False

        self.videoFormatPresets = [
            {"name": _("H.264"), "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -pix_fmt yuv420p"},
            {"name": _("H.264 (high-quality)"), "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -crf 18 -pix_fmt yuv420p"},
            {"name": _("MPEG-4"), "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 5"},
            {"name": _("MPEG-4 (high-quality)"), "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 3"},
            {"name": _("Animated GIF"), "fileExtension": "gif", "extraVideoOptions": "-filter_complex palettegen,[v]paletteuse"},
            {"name": _("Animated GIF (grayscale)"), "fileExtension": "gif", "extraVideoOptions": "-vf format=gray"}]

        self.watermarkPositionPresets = [
            {"name": "bottom-left", "position": lambda capturedImageSize, watermarkSize, spacing: [-2, -2]},
            {"name": "bottom-right", "position": lambda capturedImageSize, watermarkSize, spacing: [
                -capturedImageSize[0] * spacing + watermarkSize[0] + 2, -2]},
            {"name": "top-left", "position": lambda capturedImageSize, watermarkSize, spacing: [
                -2, -capturedImageSize[1] * spacing + watermarkSize[1] + 2]},
            {"name": "top-right", "position": lambda capturedImageSize, watermarkSize, spacing: [
                -capturedImageSize[0] * spacing + watermarkSize[0] + 2, -capturedImageSize[1] * spacing + watermarkSize[1] + 2]}]

        self.watermarkPosition = -1
        self.watermarkSizePercent = 100
        self.watermarkOpacityPercent = 100
        self.watermarkImagePath = None

    def requestCancel(self):
        logging.info("User requested cancelling of capture")
        self.cancelRequested = True

    def addLog(self, text):
        logging.info(text)
        if self.logCallback:
            self.logCallback(text)

    def showViewControllers(self, show):
        slicer.util.setViewControllersVisible(show)

    def getRandomFilePattern(self):
        import string
        import random

        numberOfRandomChars = 5
        randomString = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(numberOfRandomChars))
        filePathPattern = "tmp-" + randomString + "-%05d.png"
        return filePathPattern

    def isFfmpegPathValid(self):
        import os

        ffmpegPath = self.getFfmpegPath()
        return os.path.isfile(ffmpegPath)

    def getDownloadedFfmpegDirectory(self):
        return os.path.dirname(slicer.app.slicerUserSettingsFilePath) + "/ffmpeg"

    def getFfmpegExecutableFilename(self):
        if os.name == "nt":
            return "ffmpeg.exe"
        else:
            return "ffmpeg"

    def findFfmpeg(self):
        # Try to find the executable at specific paths
        commonFfmpegPaths = [
            "/usr/local/bin/ffmpeg",
            "/usr/bin/ffmpeg",
        ]
        for ffmpegPath in commonFfmpegPaths:
            if os.path.isfile(ffmpegPath):
                # found one
                self.setFfmpegPath(ffmpegPath)
                return True
        # Search for the executable in directories
        commonFfmpegDirs = [
            self.getDownloadedFfmpegDirectory(),
        ]
        for ffmpegDir in commonFfmpegDirs:
            if self.findFfmpegInDirectory(ffmpegDir):
                # found it
                return True
        # Not found
        return False

    def findFfmpegInDirectory(self, ffmpegDir):
        ffmpegExecutableFilename = self.getFfmpegExecutableFilename()
        for dirpath, dirnames, files in os.walk(ffmpegDir):
            for name in files:
                if name == ffmpegExecutableFilename:
                    ffmpegExecutablePath = (dirpath + "/" + name).replace("\\", "/")
                    self.setFfmpegPath(ffmpegExecutablePath)
                    return True
        return False

    def unzipFfmpeg(self, filePath, ffmpegTargetDirectory):
        if not os.path.exists(filePath) or os.stat(filePath).st_size == 0:
            logging.info("ffmpeg package is not found at " + filePath)
            return False

        logging.info("Unzipping ffmpeg package " + filePath)
        qt.QDir().mkpath(ffmpegTargetDirectory)
        slicer.app.applicationLogic().Unzip(filePath, ffmpegTargetDirectory)
        success = self.findFfmpegInDirectory(ffmpegTargetDirectory)
        return success

    def ffmpegDownload(self):
        ffmpegTargetDirectory = self.getDownloadedFfmpegDirectory()
        # The number in the filePath can be incremented each time a significantly different ffmpeg version
        # is to be introduced (it prevents reusing a previously downloaded package).
        filePath = slicer.app.temporaryPath + "/ffmpeg-package-slicer-01.zip"
        success = self.unzipFfmpeg(filePath, ffmpegTargetDirectory)
        if success:
            # there was a valid downloaded package already
            return True

        # List of mirror sites to attempt download ffmpeg pre-built binaries from
        urls = []
        if os.name == "nt":
            urls.append("https://github.com/Slicer/SlicerBinaryDependencies/releases/download/ffmpeg/ffmpeg-2021-05-16-win64.zip")
        else:
            # TODO: implement downloading for Linux/MacOS?
            pass

        success = False
        qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)

        for url in urls:
            success = True
            try:
                logging.info("Requesting download ffmpeg from %s..." % url)

                import urllib.request, urllib.error, urllib.parse

                req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
                data = urllib.request.urlopen(req).read()
                with open(filePath, "wb") as f:
                    f.write(data)

                success = self.unzipFfmpeg(filePath, ffmpegTargetDirectory)
            except:
                success = False

            if success:
                break

        qt.QApplication.restoreOverrideCursor()
        return success

    def getFfmpegPath(self):
        settings = qt.QSettings()
        if settings.contains("General/ffmpegPath"):
            return slicer.app.toSlicerHomeAbsolutePath(settings.value("General/ffmpegPath"))
        return ""

    def setFfmpegPath(self, ffmpegPath):
        # don't save it if already saved
        settings = qt.QSettings()
        if settings.contains("General/ffmpegPath"):
            if ffmpegPath == slicer.app.toSlicerHomeAbsolutePath(settings.value("General/ffmpegPath")):
                return
        settings.setValue("General/ffmpegPath", slicer.app.toSlicerHomeRelativePath(ffmpegPath))

    def setWatermarkPosition(self, watermarkPosition):
        self.watermarkPosition = watermarkPosition

    def setWatermarkSizePercent(self, watermarkSizePercent):
        self.watermarkSizePercent = watermarkSizePercent

    def setWatermarkOpacityPercent(self, watermarkOpacityPercent):
        self.watermarkOpacityPercent = watermarkOpacityPercent

    def setWatermarkImagePath(self, watermarkImagePath):
        self.watermarkImagePath = watermarkImagePath

    def getSliceLogicFromSliceNode(self, sliceNode):
        lm = slicer.app.layoutManager()
        sliceLogic = lm.sliceWidget(sliceNode.GetLayoutName()).sliceLogic()
        return sliceLogic

    def getSliceOffsetRange(self, sliceNode):
        sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)

        sliceBounds = [0, -1, 0, -1, 0, -1]
        sliceLogic.GetLowestVolumeSliceBounds(sliceBounds)
        sliceOffsetMin = sliceBounds[4]
        sliceOffsetMax = sliceBounds[5]

        # increase range if it is empty
        # to allow capturing even when no volumes are shown in slice views
        if sliceOffsetMin == sliceOffsetMax:
            sliceOffsetMin = sliceLogic.GetSliceOffset() - 100
            sliceOffsetMax = sliceLogic.GetSliceOffset() + 100

        return sliceOffsetMin, sliceOffsetMax

    def getSliceOffsetResolution(self, sliceNode):
        sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)

        sliceOffsetResolution = 1.0
        sliceSpacing = sliceLogic.GetLowestVolumeSliceSpacing()
        if sliceSpacing is not None and sliceSpacing[2] > 0:
            sliceOffsetResolution = sliceSpacing[2]

        return sliceOffsetResolution

    def captureImageFromView(self, view, filename=None, transparentBackground=False, volumeNode=None):
        """
        Capture an image of the specified view and store in the specified object.

        :param view: View to capture. If none, all views are captured.
        :param filename: Filename of the desired output file. If none, no file will be written.
        :param transparentBackground: Set the background to be transparent for single-view captures.
        :param volumeNode: Vector volume node to store the capture image. If none, no vector volume node will be updated.
        """
        slicer.app.processEvents()
        if view:
            if type(view) == slicer.qMRMLSliceView or type(view) == slicer.qMRMLThreeDView:
                view.forceRender()
            else:
                view.repaint()
        else:
            slicer.util.forceRenderAllViews()

        if view is None:
            if transparentBackground:
                logging.warning("Transparent background is only available for single-view capture")

            # no view is specified, capture the entire view layout

            # Simply using grabwidget on the view layout frame would grab the screen without background:
            # img = ctk.ctkWidgetsUtils.grabWidget(slicer.app.layoutManager().viewport())

            # Grab the main window and use only the viewport's area
            allViews = slicer.app.layoutManager().viewport()
            topLeft = allViews.mapTo(slicer.util.mainWindow(), allViews.rect.topLeft())
            bottomRight = allViews.mapTo(slicer.util.mainWindow(), allViews.rect.bottomRight())
            imageSize = bottomRight - topLeft

            if imageSize.x() < 2 or imageSize.y() < 2:
                # image is too small, most likely it is invalid
                raise ValueError(_("Capture image from view failed"))

            img = ctk.ctkWidgetsUtils.grabWidget(slicer.util.mainWindow(), qt.QRect(topLeft.x(), topLeft.y(), imageSize.x(), imageSize.y()))

            capturedImage = vtk.vtkImageData()
            ctk.ctkVTKWidgetsUtils.qImageToVTKImageData(img, capturedImage)

        else:
            # Capture single view

            rw = view.renderWindow()
            wti = vtk.vtkWindowToImageFilter()

            if transparentBackground:
                originalAlphaBitPlanes = rw.GetAlphaBitPlanes()
                rw.SetAlphaBitPlanes(1)
                ren = rw.GetRenderers().GetFirstRenderer()
                originalGradientBackground = ren.GetGradientBackground()
                ren.SetGradientBackground(False)
                wti.SetInputBufferTypeToRGBA()
                rw.Render()  # need to render after changing bit planes

            wti.SetInput(rw)
            wti.Update()

            if transparentBackground:
                rw.SetAlphaBitPlanes(originalAlphaBitPlanes)
                ren.SetGradientBackground(originalGradientBackground)

            capturedImage = wti.GetOutput()

        imageSize = capturedImage.GetDimensions()

        if imageSize[0] < 2 or imageSize[1] < 2:
            # image is too small, most likely it is invalid
            raise ValueError(_("Capture image from view failed"))

        # Make sure image width and height is even, otherwise encoding may fail
        imageWidthOdd = imageSize[0] & 1 == 1
        imageHeightOdd = imageSize[1] & 1 == 1
        if imageWidthOdd or imageHeightOdd:
            imageClipper = vtk.vtkImageClip()
            imageClipper.SetClipData(True)
            imageClipper.SetInputData(capturedImage)
            extent = capturedImage.GetExtent()
            imageClipper.SetOutputWholeExtent(extent[0], extent[1] - 1 if imageWidthOdd else extent[1],
                                              extent[2], extent[3] - 1 if imageHeightOdd else extent[3],
                                              extent[4], extent[5])
            imageClipper.Update()
            capturedImage = imageClipper.GetOutput()

        capturedImage = self.addWatermark(capturedImage)
        if volumeNode is not None:
            if isinstance(volumeNode, slicer.vtkMRMLVectorVolumeNode):
                ijkToRas = vtk.vtkMatrix4x4()
                ijkToRas.SetElement(0, 0, -1)
                ijkToRas.SetElement(1, 1, -1)
                volumeNode.SetIJKToRASMatrix(ijkToRas)
                vflip = vtk.vtkImageFlip()
                vflip.SetInputData(capturedImage)
                vflip.SetFilteredAxis(1)
                vflip.Update()
                volumeNode.SetAndObserveImageData(vflip.GetOutput())
            else:
                raise ValueError(_("Invalid vector volume node."))
        if filename:
            writer = self.createImageWriter(filename)
            writer.SetInputData(capturedImage)
            writer.SetFileName(filename)
            writer.Write()

    def createImageWriter(self, filename):
        _name, extension = os.path.splitext(filename)
        if extension.lower() == ".png":
            return vtk.vtkPNGWriter()
        elif extension.lower() == ".jpg" or extension.lower() == ".jpeg":
            return vtk.vtkJPEGWriter()
        else:
            raise ValueError(_("Unsupported image format based on file name {filename}").format(filename=filename))

    def createImageReader(self, filename):
        _name, extension = os.path.splitext(filename)
        if extension.lower() == ".png":
            return vtk.vtkPNGReader()
        elif extension.lower() == ".jpg" or extension.lower() == ".jpeg":
            return vtk.vtkJPEGReader()
        else:
            raise ValueError(_("Unsupported image format based on file name {filename}").format(filename=filename))

    def addWatermark(self, capturedImage):
        if self.watermarkPosition < 0:
            # no watermark
            return capturedImage

        watermarkReader = vtk.vtkPNGReader()
        watermarkReader.SetFileName(self.watermarkImagePath)
        watermarkReader.Update()
        watermarkImage = watermarkReader.GetOutput()

        # Add alpha channel, if image is only RGB and not RGBA
        if watermarkImage.GetNumberOfScalarComponents() == 3:
            alphaImage = vtk.vtkImageData()
            alphaImage.SetDimensions(watermarkImage.GetDimensions())
            alphaImage.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
            alphaImage.GetPointData().GetScalars().Fill(255)
            appendRGBA = vtk.vtkImageAppendComponents()
            appendRGBA.AddInputData(watermarkImage)
            appendRGBA.AddInputData(alphaImage)
            appendRGBA.Update()
            watermarkImage = appendRGBA.GetOutput()

        watermarkSize = [0] * 3
        watermarkImage.GetDimensions(watermarkSize)
        capturedImageSize = [0] * 3
        capturedImage.GetDimensions(capturedImageSize)
        spacing = 100.0 / self.watermarkSizePercent
        watermarkResize = vtk.vtkImageReslice()
        watermarkResize.SetInterpolationModeToCubic()
        watermarkResize.SetInputData(watermarkImage)
        watermarkResize.SetOutputExtent(capturedImage.GetExtent())
        watermarkResize.SetOutputSpacing(spacing, spacing, 1)
        position = self.watermarkPositionPresets[self.watermarkPosition]["position"](capturedImageSize, watermarkSize, spacing)
        watermarkResize.SetOutputOrigin(position[0], position[1], 0.0)
        watermarkResize.Update()

        blend = vtk.vtkImageBlend()
        blend.SetOpacity(0, 1.0 - self.watermarkOpacityPercent * 0.01)
        blend.SetOpacity(1, self.watermarkOpacityPercent * 0.01)
        blend.AddInputData(capturedImage)
        blend.AddInputData(watermarkResize.GetOutput())
        blend.Update()

        return blend.GetOutput()

    def viewFromNode(self, viewNode):
        if not viewNode:
            raise ValueError(_("Invalid view node."))
        elif viewNode.IsA("vtkMRMLSliceNode"):
            return slicer.app.layoutManager().sliceWidget(viewNode.GetLayoutName()).sliceView()
        elif viewNode.IsA("vtkMRMLViewNode"):
            renderView = None
            lm = slicer.app.layoutManager()
            for widgetIndex in range(lm.threeDViewCount):
                view = lm.threeDWidget(widgetIndex).threeDView()
                if viewNode == view.mrmlViewNode():
                    renderView = view
                    break
            if not renderView:
                raise ValueError(_("Selected 3D view is not visible in the current layout."))
            return renderView
        elif viewNode.IsA("vtkMRMLPlotViewNode"):
            renderView = None
            lm = slicer.app.layoutManager()
            for viewIndex in range(lm.plotViewCount):
                if viewNode == lm.plotWidget(viewIndex).mrmlPlotViewNode():
                    renderView = lm.plotWidget(viewIndex).plotView()
                    break
            if not renderView:
                raise ValueError(_("Selected 3D view is not visible in the current layout."))
            return renderView
        else:
            raise ValueError(_("Invalid view node."))

    def captureSliceSweep(self, sliceNode, startSliceOffset, endSliceOffset, numberOfImages,
                          outputDir, outputFilenamePattern, captureAllViews=None, transparentBackground=False):

        self.cancelRequested = False

        if not captureAllViews and not sliceNode.IsMappedInLayout():
            raise ValueError(_("Selected slice view is not visible in the current layout."))

        if not os.path.exists(outputDir):
            os.makedirs(outputDir)
        filePathPattern = os.path.join(outputDir, outputFilenamePattern)

        sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
        originalSliceOffset = sliceLogic.GetSliceOffset()

        sliceView = self.viewFromNode(sliceNode)
        compositeNode = sliceLogic.GetSliceCompositeNode()
        offsetStepSize = (endSliceOffset - startSliceOffset) / (numberOfImages - 1)
        for offsetIndex in range(numberOfImages):
            filename = filePathPattern % offsetIndex
            self.addLog(_("Write {filename}").format(filename=filename))
            sliceLogic.SetSliceOffset(startSliceOffset + offsetIndex * offsetStepSize)
            self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground)
            if self.cancelRequested:
                break

        sliceLogic.SetSliceOffset(originalSliceOffset)
        if self.cancelRequested:
            raise ValueError(_("User requested cancel."))

    def captureSliceFade(self, sliceNode, numberOfImages, outputDir,
                         outputFilenamePattern, captureAllViews=None, transparentBackground=False):

        self.cancelRequested = False

        if not captureAllViews and not sliceNode.IsMappedInLayout():
            raise ValueError(_("Selected slice view is not visible in the current layout."))

        if not os.path.exists(outputDir):
            os.makedirs(outputDir)
        filePathPattern = os.path.join(outputDir, outputFilenamePattern)

        sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
        sliceView = self.viewFromNode(sliceNode)
        compositeNode = sliceLogic.GetSliceCompositeNode()
        originalForegroundOpacity = compositeNode.GetForegroundOpacity()
        startForegroundOpacity = 0.0
        endForegroundOpacity = 1.0
        opacityStepSize = (endForegroundOpacity - startForegroundOpacity) / (numberOfImages - 1)
        for offsetIndex in range(numberOfImages):
            filename = filePathPattern % offsetIndex
            self.addLog(_("Write {filename}").format(filename=filename))
            compositeNode.SetForegroundOpacity(startForegroundOpacity + offsetIndex * opacityStepSize)
            self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground)
            if self.cancelRequested:
                break

        compositeNode.SetForegroundOpacity(originalForegroundOpacity)

        if self.cancelRequested:
            raise ValueError(_("User requested cancel."))

    def capture3dViewRotation(self, viewNode, startRotation, endRotation, numberOfImages, rotationAxis,
                              outputDir, outputFilenamePattern, captureAllViews=None, transparentBackground=False):
        """Acquire a set of screenshots of the 3D view while rotating it."""

        self.cancelRequested = False

        if not os.path.exists(outputDir):
            os.makedirs(outputDir)
        filePathPattern = os.path.join(outputDir, outputFilenamePattern)

        renderView = self.viewFromNode(viewNode)

        # Save original orientation and go to start orientation
        originalPitchRollYawIncrement = renderView.pitchRollYawIncrement
        originalDirection = renderView.pitchDirection
        renderView.setPitchRollYawIncrement(-startRotation)
        if rotationAxis == AXIS_YAW:
            renderView.yawDirection = renderView.YawRight
            renderView.yaw()
        else:
            renderView.pitchDirection = renderView.PitchDown
            renderView.pitch()

        # Rotate step-by-step
        rotationStepSize = (endRotation - startRotation) / (numberOfImages - 1)
        renderView.setPitchRollYawIncrement(rotationStepSize)
        if rotationAxis == AXIS_YAW:
            renderView.yawDirection = renderView.YawLeft
        else:
            renderView.pitchDirection = renderView.PitchUp
        for offsetIndex in range(numberOfImages):
            if not self.cancelRequested:
                filename = filePathPattern % offsetIndex
                self.addLog(_("Write {filename}").format(filename=filename))
                self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground)
            if rotationAxis == AXIS_YAW:
                renderView.yaw()
            else:
                renderView.pitch()

        # Restore original orientation and rotation step size & direction
        if rotationAxis == AXIS_YAW:
            renderView.yawDirection = renderView.YawRight
            renderView.yaw()
            renderView.setPitchRollYawIncrement(endRotation)
            renderView.yaw()
            renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement)
            renderView.yawDirection = originalDirection
        else:
            renderView.pitchDirection = renderView.PitchDown
            renderView.pitch()
            renderView.setPitchRollYawIncrement(endRotation)
            renderView.pitch()
            renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement)
            renderView.pitchDirection = originalDirection

        if self.cancelRequested:
            raise ValueError(_("User requested cancel."))

    def captureSequence(self, viewNode, sequenceBrowserNode, sequenceStartIndex,
                        sequenceEndIndex, numberOfImages, outputDir, outputFilenamePattern,
                        captureAllViews=None, transparentBackground=False):
        """Acquire a set of screenshots of a view while iterating through a sequence."""

        self.cancelRequested = False

        if not os.path.exists(outputDir):
            os.makedirs(outputDir)
        filePathPattern = os.path.join(outputDir, outputFilenamePattern)

        originalSelectedItemNumber = sequenceBrowserNode.GetSelectedItemNumber()

        renderView = self.viewFromNode(viewNode)
        stepSize = (sequenceEndIndex - sequenceStartIndex) / (numberOfImages - 1)
        for offsetIndex in range(numberOfImages):
            sequenceBrowserNode.SetSelectedItemNumber(int(sequenceStartIndex + offsetIndex * stepSize))
            filename = filePathPattern % offsetIndex
            self.addLog(_("Write {filename}").format(filename=filename))
            self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground)
            if self.cancelRequested:
                break

        sequenceBrowserNode.SetSelectedItemNumber(originalSelectedItemNumber)
        if self.cancelRequested:
            raise ValueError(_("User requested cancel."))

    def createLightboxImage(self, numberOfColumns, outputDir, imageFileNamePattern, numberOfImages, lightboxImageFilename):
        self.addLog(_("Export to lightbox image..."))
        filePathPattern = os.path.join(outputDir, imageFileNamePattern)

        import math

        numberOfRows = math.ceil(numberOfImages / numberOfColumns)
        imageMarginSizePixels = 5
        for row in range(numberOfRows):
            for column in range(numberOfColumns):
                imageIndex = row * numberOfColumns + column
                if imageIndex >= numberOfImages:
                    break
                sourceFilename = filePathPattern % imageIndex
                reader = self.createImageReader(sourceFilename)
                reader.SetFileName(sourceFilename)
                reader.Update()
                image = reader.GetOutput()

                if imageIndex == 0:
                    # First image, initialize output lightbox image
                    imageDimensions = image.GetDimensions()
                    lightboxImageExtent = [0, numberOfColumns * imageDimensions[0] + (numberOfColumns - 1) * imageMarginSizePixels - 1,
                                           0, numberOfRows * imageDimensions[1] + (numberOfRows - 1) * imageMarginSizePixels - 1,
                                           0, 0]
                    lightboxCanvas = vtk.vtkImageCanvasSource2D()
                    lightboxCanvas.SetNumberOfScalarComponents(3)
                    lightboxCanvas.SetScalarTypeToUnsignedChar()
                    lightboxCanvas.SetExtent(lightboxImageExtent)
                    # Fill background with black
                    lightboxCanvas.SetDrawColor(50, 50, 50)
                    lightboxCanvas.FillBox(*lightboxImageExtent[0:4])

                drawingPosition = [column * (imageDimensions[0] + imageMarginSizePixels),
                                   lightboxImageExtent[3] - (row + 1) * imageDimensions[1] - row * imageMarginSizePixels + 1]
                lightboxCanvas.DrawImage(drawingPosition[0], drawingPosition[1], image)

        lightboxCanvas.Update()
        outputLightboxImageFilePath = os.path.join(outputDir, lightboxImageFilename)
        writer = self.createImageWriter(outputLightboxImageFilePath)
        writer.SetFileName(outputLightboxImageFilePath)
        writer.SetInputData(lightboxCanvas.GetOutput())
        writer.Write()

        self.addLog(_("Lightbox image saved to file: {filename}").format(filename=outputLightboxImageFilePath))

    def createVideo(self, frameRate, extraOptions, outputDir, imageFileNamePattern, videoFileName):
        self.addLog(_("Export to video..."))

        # Get ffmpeg
        import os.path

        ffmpegPath = os.path.abspath(self.getFfmpegPath())
        if not ffmpegPath:
            raise ValueError(_("Video creation failed: ffmpeg executable path is not defined"))
        if not os.path.isfile(ffmpegPath):
            raise ValueError(_("Video creation failed: ffmpeg executable path is invalid: {path}").format(filepath=ffmpegPath))

        filePathPattern = os.path.join(outputDir, imageFileNamePattern)
        outputVideoFilePath = os.path.join(outputDir, videoFileName)
        ffmpegParams = [ffmpegPath,
                        "-nostdin",  # disable stdin (to prevent hang when running Slicer as background process)
                        "-y",  # overwrite without asking
                        "-r", str(frameRate),
                        "-start_number", "0",
                        "-i", str(filePathPattern)]
        ffmpegParams += [_f for _f in extraOptions.split(" ") if _f]
        ffmpegParams.append(outputVideoFilePath)

        self.addLog(_("Start ffmpeg:") + "\n" + " ".join(ffmpegParams))

        import subprocess

        p = subprocess.Popen(ffmpegParams, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=outputDir)
        stdout, stderr = p.communicate()
        if p.returncode != 0:
            self.addLog(_("ffmpeg error output: {error}").format(error=stderr.decode()))
            raise ValueError(_("ffmpeg returned with error"))
        else:
            self.addLog(_("Video export succeeded to file: {path}").format(path=outputVideoFilePath))
            logging.debug("ffmpeg standard output: " + stdout.decode())
            logging.debug("ffmpeg error output: " + stderr.decode())

    def deleteTemporaryFiles(self, outputDir, imageFileNamePattern, numberOfImages):
        """Delete files after a video has been created from them."""
        import os

        filePathPattern = os.path.join(outputDir, imageFileNamePattern)
        for imageIndex in range(numberOfImages):
            filename = filePathPattern % imageIndex
            logging.debug("Delete temporary file " + filename)
            os.remove(filename)

    def getNextAvailableFileName(self, outputDir, outputFilenamePattern, snapshotIndex):
        """Find a file index that does not overwrite any existing file."""
        if not os.path.exists(outputDir):
            os.makedirs(outputDir)
        filePathPattern = os.path.join(outputDir, outputFilenamePattern)
        filename = None
        while True:
            filename = filePathPattern % snapshotIndex
            if not os.path.exists(filename):
                # found an available file name
                break
            snapshotIndex += 1
        return [filename, snapshotIndex]


class ScreenCaptureTest(ScriptedLoadableModuleTest):
    """
    This is the test case for your scripted module.
    Uses ScriptedLoadableModuleTest base class, available at:
    https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py
    """

    def setUp(self):
        """Do whatever is needed to reset the state - typically a scene clear will be enough."""
        slicer.mrmlScene.Clear(0)

        import SampleData

        self.image1 = SampleData.downloadSample("MRBrainTumor1")
        self.image2 = SampleData.downloadSample("MRBrainTumor2")

        # make the output volume appear in all the slice views
        selectionNode = slicer.app.applicationLogic().GetSelectionNode()
        selectionNode.SetActiveVolumeID(self.image1.GetID())
        selectionNode.SetSecondaryVolumeID(self.image2.GetID())
        slicer.app.applicationLogic().PropagateVolumeSelection(1)

        # Show slice and 3D views
        layoutManager = slicer.app.layoutManager()
        layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView)
        for sliceViewNodeId in ["vtkMRMLSliceNodeRed", "vtkMRMLSliceNodeYellow", "vtkMRMLSliceNodeGreen"]:
            slicer.mrmlScene.GetNodeByID(sliceViewNodeId).SetSliceVisible(True)

        self.tempDir = slicer.app.temporaryPath + "/ScreenCaptureTest"
        self.numberOfImages = 10
        self.imageFileNamePattern = "image_%05d.png"

        self.logic = ScreenCaptureLogic()

    def verifyAndDeleteWrittenFiles(self):
        import os

        filePathPattern = os.path.join(self.tempDir, self.imageFileNamePattern)
        for imageIndex in range(self.numberOfImages):
            filename = filePathPattern % imageIndex
            self.assertTrue(os.path.exists(filename))
        self.logic.deleteTemporaryFiles(self.tempDir, self.imageFileNamePattern, self.numberOfImages)
        for imageIndex in range(self.numberOfImages):
            filename = filePathPattern % imageIndex
            self.assertFalse(os.path.exists(filename))

    def runTest(self):
        """Run as few or as many tests as needed here."""
        self.setUp()
        self.test_SliceSweep()
        self.test_SliceFade()
        self.test_3dViewRotation()
        self.test_VolumeNodeUpdate()

    def test_SliceSweep(self):
        self.delayDisplay("Testing SliceSweep")
        viewNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSliceNodeRed")
        self.assertIsNotNone(viewNode)
        self.logic.captureSliceSweep(viewNode, -125, 75, self.numberOfImages, self.tempDir, self.imageFileNamePattern)
        self.verifyAndDeleteWrittenFiles()
        self.delayDisplay("Testing SliceSweep completed successfully")

    def test_SliceFade(self):
        self.delayDisplay("Testing SliceFade")
        viewNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSliceNodeRed")
        self.assertIsNotNone(viewNode)
        self.logic.captureSliceFade(viewNode, self.numberOfImages, self.tempDir, self.imageFileNamePattern)
        self.verifyAndDeleteWrittenFiles()
        self.delayDisplay("Testing SliceFade completed successfully")

    def test_3dViewRotation(self):
        self.delayDisplay("Testing 3D view rotation")
        viewNode = slicer.mrmlScene.GetNodeByID("vtkMRMLViewNode1")
        self.assertIsNotNone(viewNode)
        self.logic.capture3dViewRotation(viewNode, -180, 180, self.numberOfImages, AXIS_YAW, self.tempDir, self.imageFileNamePattern)
        self.verifyAndDeleteWrittenFiles()
        self.delayDisplay("Testing 3D view rotation completed successfully")

    def test_VolumeNodeUpdate(self):
        self.delayDisplay("Testing VolumeNode update")
        viewNode = None  # Capture All Views
        volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode")
        self.assertIsNotNone(volumeNode)
        self.assertIsNone(volumeNode.GetImageData())
        self.logic.captureImageFromView(viewNode, volumeNode=volumeNode)
        self.assertIsNotNone(volumeNode.GetImageData())
        self.delayDisplay("Testing VolumeNode update completed successfully")
