import glob
import os
import time
from tempfile import NamedTemporaryFile, TemporaryDirectory
from typing import Any, List, Tuple, Union

import numpy as np

import vedo.vtkclasses as vtki  # a wrapper for lazy imports

import vedo
from vedo import settings
from vedo import colors
from vedo import utils
from vedo.assembly import Assembly
from vedo.image import Image
from vedo.pointcloud import Points
from vedo.mesh import Mesh
from vedo.volume import Volume

__docformat__ = "google"

__doc__ = """
Submodule to read/write meshes and other objects in different formats,
and other I/O functionalities.
"""

__all__ = [
    "load",
    "read",
    "download",
    "gunzip",
    "loadStructuredPoints",
    "loadStructuredGrid",
    "write",
    "save",
    "export_window",
    "import_window",
    "load_obj",
    "screenshot",
    "ask",
    "Video",
]


# example web page for X3D
_x3d_html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title> vedo with x3d </title>

  <!-- THESE ARE THE RELEVANT LINES: -->
  <script src='https://www.x3dom.org/download/x3dom.js'> </script>
  <link rel='stylesheet' type='text/css' href='https://www.x3dom.org/download/x3dom.css'/>

  <style>
     table, td, th { border: 1px solid black; background-color: powderblue;}
     table {width: 70%; border-collapse: collapse;}
     table th {width: 35%;}
  </style>
</head>

<body style="font-family: Verdana">
  <h1>Example html generated by vedo</h1>
  This example loads a 3D scene from file ~fileoutput generated by
  <a href="https://github.com/marcomusy/vedo">vedo</a>
  (see <a href="https://github.com/marcomusy/vedo/tree/master/examples/other/export_x3d.py">export_x3d.py</a>).
  <br><br>


  <!-- THESE ARE THE RELEVANT LINES: -->
  <x3d width='~widthpx' height='~heightpx'>
     <scene>
        <Inline url="~fileoutput"> </Inline>
     </scene>
  </x3d>

  <h3>Nothing shows up above this line?</h3>
  Enable your browser to load local files:
  <br><b>Firefox</b>: type <code>about:config</code> in the URL bar and
  change <code>privacy.file_unique_origin</code> from <code>True</code> to <code>False</code>
  <br><b>Chrome</b>: from terminal type:
  <code>google-chrome --enable-webgl --allow-file-access-from-files</code>
  (see <a href="https://cmatskas.com/interacting-with-local-data-files-using-chrome/">here</a>)

  <br>
  <h3>Controls:</h3>
  <h4><strong>Examine Mode (activate with key 'e'):</strong></h4>
  <table>
     <tbody>
        <tr class="even description">
           <th>Button</th>
           <th>Function</th>
        </tr>
        <tr>
           <td>Left Button / Left Button + Shift</td>
           <td>Rotate</td>
        </tr>
        <tr>
           <td>Mid Button / Left Button + Ctl</td>
           <td>Pan</td>
        </tr>
        <tr>
           <td>Right Button / Wheel / Left Button + Alt</td>
           <td>Zoom</td>
        </tr>
        <tr>
           <td>Left double click</td>
           <td>Set center of rotation</td>
        </tr>
     </tbody>
  </table>
  <h4><strong>Walk Mode (activate with key 'w'):</strong></h4>
  <table>
     <tbody>
        <tr class="even description">
           <th>Button</th>
           <th>Function</th>
        </tr>
        <tr>
           <td>Left Button</td>
           <td>Move forward</td>
        </tr>
        <tr>
           <td>Right Button</td>
           <td>Move backward</td>
        </tr>
     </tbody>
  </table>
  <h4><strong>Fly Mode (activate with key 'f'):</strong></h4>
  <table>
     <tbody>
        <tr class="even description">
           <th>Button</th>
           <th>Function</th>
        </tr>
        <tr>
           <td>Left Button</td>
           <td>Move forward</td>
        </tr>
        <tr>
           <td>Right Button</td>
           <td>Move backward</td>
        </tr>
     </tbody>
  </table>
  <h3>Non-interactive camera movement</h3>
  <table>
     <tbody>
        <tr class="even description">
           <th>Key</th>
           <th>Function</th>
        </tr>
        <tr>
           <td>r</td>
           <td>reset view</td>
        </tr>
        <tr>
           <td>a</td>
           <td>show all</td>
        </tr>
        <tr>
           <td>u</td>
           <td>upright</td>
        </tr>
     </tbody>
  </table>
</body>
</html>
"""

########################################################################
def load(inputobj: Union[list, str, os.PathLike], unpack=True, force=False) -> Any:
    """
    Load any vedo objects from file or from the web.

    The output will depend on the file extension. See examples below.
    Unzip is made on the fly, if file ends with `.gz`.
    Can load an object directly from a URL address.

    Arguments:
        unpack : (bool)
            unpack MultiBlockData into a flat list of objects.
        force : (bool)
            when downloading a file ignore any previous cached downloads and force a new one.

    Example:
        ```python
        from vedo import dataurl, load, show
        # Return a list of 2 meshes
        g = load([dataurl+'250.vtk', dataurl+'270.vtk'])
        show(g)
        # Return a list of meshes by reading all files in a directory
        # (if directory contains DICOM files then a Volume is returned)
        g = load('mydicomdir/')
        show(g)
        ```
    """
    if isinstance(inputobj, list):
        inputobj = [str(f) for f in inputobj]
    else:
        inputobj = str(inputobj)

    acts = []
    if utils.is_sequence(inputobj):
        flist = inputobj
    elif isinstance(inputobj, str) and inputobj.startswith("https://"):
        flist = [inputobj]
    else:
        flist = utils.humansort(glob.glob(inputobj))

    for fod in flist:

        if fod.startswith("https://"):
            fod = download(fod, force=force, verbose=False)

        if os.path.isfile(fod):  ### it's a file

            if fod.endswith(".gz"):
                fod = gunzip(fod)

            a = _load_file(fod, unpack)
            acts.append(a)

        elif os.path.isdir(fod):  ### it's a directory or DICOM
            flist = os.listdir(fod)
            if ".dcm" in flist[0]:  ### it's DICOM
                reader = vtki.new("DICOMImageReader")
                reader.SetDirectoryName(fod)
                reader.Update()
                image = reader.GetOutput()
                vol = Volume(image)
                try:
                    vol.metadata["PixelSpacing"] = reader.GetPixelSpacing()
                    vol.metadata["Width"] = reader.GetWidth()
                    vol.metadata["Height"] = reader.GetHeight()
                    vol.metadata["PositionPatient"] = reader.GetImagePositionPatient()
                    vol.metadata["OrientationPatient"] = reader.GetImageOrientationPatient()
                    vol.metadata["BitsAllocated"] = reader.GetBitsAllocated()
                    vol.metadata["PixelRepresentation"] = reader.GetPixelRepresentation()
                    vol.metadata["NumberOfComponents"] = reader.GetNumberOfComponents()
                    vol.metadata["TransferSyntaxUID"] = reader.GetTransferSyntaxUID()
                    vol.metadata["RescaleSlope"] = reader.GetRescaleSlope()
                    vol.metadata["RescaleOffset"] = reader.GetRescaleOffset()
                    vol.metadata["PatientName"] = reader.GetPatientName()
                    vol.metadata["StudyUID"] = reader.GetStudyUID()
                    vol.metadata["StudyID"] = reader.GetStudyID()
                    vol.metadata["GantryAngle"] = reader.GetGantryAngle()
                except Exception as e:
                    vedo.logger.warning(f"Cannot read DICOM metadata: {e}")
                acts.append(vol)

            else:  ### it's a normal directory
                utils.humansort(flist)
                for ifile in flist:
                    a = _load_file(fod + "/" + ifile, unpack)
                    acts.append(a)
        else:
            vedo.logger.error(f"in load(), cannot find {fod}")

    if len(acts) == 1:
        if "numpy" in str(type(acts[0])):
            return acts[0]
        if not acts[0]:
            vedo.logger.error(f"in load(), cannot load {inputobj}")
        return acts[0]

    if len(acts) == 0:
        vedo.logger.error(f"in load(), cannot load {inputobj}")
        return None

    else:
        return acts

########################################################################
def _load_file(filename, unpack):
    fl = str(filename).lower()

    ########################################################## other formats:
    if fl.endswith(".xml") or fl.endswith(".xml.gz") or fl.endswith(".xdmf"):
        # Fenics tetrahedral file
        objt = loadDolfin(filename)
    elif fl.endswith(".neutral") or fl.endswith(".neu"):  # neutral tets
        objt = loadNeutral(filename)
    elif fl.endswith(".gmsh"):  # gmesh file
        objt = loadGmesh(filename)
    elif fl.endswith(".pcd"):  # PCL point-cloud format
        objt = loadPCD(filename)
        objt.properties.SetPointSize(2)
    elif fl.endswith(".off"):
        objt = loadOFF(filename)
    elif fl.endswith(".step") or fl.endswith(".stp"):
        objt = loadSTEP(filename)
    elif fl.endswith(".3ds"):  # 3ds format
        objt = load3DS(filename)
    elif fl.endswith(".wrl"):
        importer = vtki.new("VRMLImporter")
        importer.SetFileName(filename)
        importer.Read()
        importer.Update()
        actors = importer.GetRenderer().GetActors()  # vtkActorCollection
        actors.InitTraversal()
        wacts = []
        for i in range(actors.GetNumberOfItems()):
            act = actors.GetNextActor()
            m = Mesh(act.GetMapper().GetInput())
            m.actor = act
            wacts.append(m)
        objt = Assembly(wacts)
    elif fl.endswith(".glb") or fl.endswith(".gltf"):
        importer = vtki.new("GLTFImporter")
        importer.SetFileName(filename)
        importer.Update()
        actors = importer.GetRenderer().GetActors()  # vtkActorCollection
        actors.InitTraversal()
        wacts = []
        for i in range(actors.GetNumberOfItems()):
            act = actors.GetNextActor()
            m = Mesh(act.GetMapper().GetInput())
            m.actor = act
            wacts.append(m)
        objt = Assembly(wacts)

    ######################################################## volumetric:
    elif fl.endswith((".tif", ".tiff", ".slc", ".vti", ".mhd", ".nrrd", ".nii", ".dem")):
        img = loadImageData(filename)
        objt = Volume(img)

    ######################################################### 2D images:
    elif fl.endswith((".jpg", ".jpeg",".png", ".bmp")):
        if ".png" in fl:
            picr = vtki.new("PNGReader")
        elif ".jpg" in fl or ".jpeg" in fl:
            picr = vtki.new("JPEGReader")
        elif ".bmp" in fl:
            picr = vtki.new("BMPReader")
        elif ".gif" in fl:
            from PIL import Image as PILImage, ImageSequence

            img = PILImage.open(filename)
            frames = []
            for frame in ImageSequence.Iterator(img):
                a = np.array(frame.convert("RGB").getdata(), dtype=np.uint8)
                a = a.reshape([frame.size[1], frame.size[0], 3])
                frames.append(Image(a))
            return frames

        picr.SetFileName(filename)
        picr.Update()
        objt = Image(picr.GetOutput())

    ######################################################### multiblock:
    elif fl.endswith(".vtm") or fl.endswith(".vtmb"):
        mbread = vtki.new("XMLMultiBlockDataReader")
        mbread.SetFileName(filename)
        mbread.Update()
        mb = mbread.GetOutput()
        if unpack:
            acts = []
            for i in range(mb.GetNumberOfBlocks()):
                b = mb.GetBlock(i)
                if isinstance(
                    b,
                    (
                        vtki.vtkPolyData,
                        vtki.vtkStructuredGrid,
                        vtki.vtkRectilinearGrid,
                    ),
                ):
                    acts.append(Mesh(b))
                elif isinstance(b, vtki.vtkImageData):
                    acts.append(Volume(b))
                elif isinstance(b, vtki.vtkUnstructuredGrid):
                    acts.append(vedo.UnstructuredGrid(b))
            return acts
        return mb

    ######################################################### assembly:
    elif fl.endswith(".npy"):
        data = np.load(filename, allow_pickle=True)
        try:
            # old format with a single object
            meshs = [from_numpy(dd) for dd in data]
        except TypeError:
            data = data.item()
            meshs = []
            for ad in data["objects"][0]["parts"]:
                obb = from_numpy(ad)
                meshs.append(obb)
        return Assembly(meshs)

    ###########################################################
    elif fl.endswith(".geojson"):
        return loadGeoJSON(filename)

    elif fl.endswith(".pvd"):
        return loadPVD(filename)

    ########################################################### polygonal mesh:
    else:
        if fl.endswith(".vtk"):  # read all legacy vtk types
            reader = vtki.new("DataSetReader")
            reader.ReadAllScalarsOn()
            reader.ReadAllVectorsOn()
            reader.ReadAllTensorsOn()
            reader.ReadAllFieldsOn()
            reader.ReadAllNormalsOn()
            reader.ReadAllColorScalarsOn()
        elif fl.endswith(".ply"):
            reader = vtki.new("PLYReader")
        elif fl.endswith(".obj"):
            reader = vtki.new("OBJReader")
            reader.SetGlobalWarningDisplay(0) # suppress warnings issue #980
        elif fl.endswith(".stl"):
            reader = vtki.new("STLReader")
        elif fl.endswith(".byu") or fl.endswith(".g"):
            reader = vtki.new("BYUReader")
        elif fl.endswith(".foam"):  # OpenFoam
            reader = vtki.new("OpenFOAMReader")
        elif fl.endswith(".pvd"):
            reader = vtki.new("XMLGenericDataObjectReader")
        elif fl.endswith(".vtp"):
            reader = vtki.new("XMLPolyDataReader")
        elif fl.endswith(".vts"):
            reader = vtki.new("XMLStructuredGridReader")
        elif fl.endswith(".vtu"):
            reader = vtki.new("XMLUnstructuredGridReader")
        elif fl.endswith(".vtr"):
            reader = vtki.new("XMLRectilinearGridReader")
        elif fl.endswith(".pvtr"):
            reader = vtki.new("XMLPRectilinearGridReader")
        elif fl.endswith("pvtu"):
            reader = vtki.new("XMLPUnstructuredGridReader")
        elif fl.endswith(".txt") or fl.endswith(".xyz") or fl.endswith(".dat"):
            reader = vtki.new("ParticleReader")  # (format is x, y, z, scalar)
        elif fl.endswith(".facet"):
            reader = vtki.new("FacetReader")
        else:
            return None

        reader.SetFileName(filename)
        reader.Update()
        routput = reader.GetOutput()

        if not routput:
            vedo.logger.error(f"unable to load {filename}")
            return None

        if isinstance(routput, vtki.vtkUnstructuredGrid):
            objt = vedo.UnstructuredGrid(routput)

        else:
            objt = Mesh(routput)
            if fl.endswith(".txt") or fl.endswith(".xyz") or fl.endswith(".dat"):
                objt.point_size(4)

    objt.filename = filename
    objt.file_size, objt.created = file_info(filename)
    return objt


def download(url: str, force=False, verbose=True) -> str:
    """
    Retrieve a file from a URL, save it locally and return its path.
    Use `force=True` to force a reload and discard cached copies.
    """
    if not url.startswith("https://"):
        # assume it's a file so no need to download
        return url
    url = url.replace("www.dropbox", "dl.dropbox")

    if "github.com" in url:
        url = url.replace("/blob/", "/raw/")

    basename = os.path.basename(url)

    if "?" in basename:
        basename = basename.split("?")[0]

    home_directory = os.path.expanduser("~")
    cachedir = os.path.join(home_directory, settings.cache_directory, "vedo")
    fname = os.path.join(cachedir, basename)
    # Create the directory if it does not exist
    if not os.path.exists(cachedir):
        os.makedirs(cachedir)

    if not force and os.path.exists(fname):
        if verbose:
            colors.printc("reusing cached file:", fname)
        return fname

    try:
        from urllib.request import urlopen, Request
        req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
        if verbose:
            colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="")

    except ImportError:
        import urllib2 # type: ignore
        import contextlib
        urlopen = lambda url_: contextlib.closing(urllib2.urlopen(url_))
        req = url
        if verbose:
            colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="")

    with urlopen(req) as response, open(fname, "wb") as output:
        output.write(response.read())

    if verbose:
        colors.printc(" done.")
    return fname


########################################################################
# def download_new(url, to_local_file="", force=False, verbose=True):
#     """
#     Downloads a file from `url` to `to_local_file` if the local copy is outdated.

#     Arguments:
#         url : (str)
#             The URL to download the file from.
#         to_local_file : (str)
#             The local file name to save the file to.
#             If not specified, the file name will be the same as the remote file name
#             in the directory specified by `settings.cache_directory + "/vedo"`.
#         force : (bool)
#             Force a new download even if the local file is up to date.
#         verbose : (bool)
#             Print verbose messages.
#     """
#     if not url.startswith("https://"):
#         if os.path.exists(url):
#             # Assume the url is already the local file path
#             return url
#         else:
#             raise FileNotFoundError(f"File not found: {url}")

#     from datetime import datetime
#     import requests

#     url = url.replace("www.dropbox", "dl.dropbox")

#     if "github.com" in url:
#         url = url.replace("/blob/", "/raw/")

#     # Get the user's home directory
#     home_directory = os.path.expanduser("~")

#     # Define the path for the cache directory
#     cachedir = os.path.join(home_directory, settings.cache_directory, "vedo")

#     # Create the directory if it does not exist
#     if not os.path.exists(cachedir):
#         os.makedirs(cachedir)

#     if not to_local_file:
#         basename = os.path.basename(url)
#         if "?" in basename:
#             basename = basename.split("?")[0]
#         to_local_file = os.path.join(cachedir, basename)
#         if verbose: print(f"Using local file name: {to_local_file}")

#     # Check if the local file exists and get its last modified time
#     if os.path.exists(to_local_file):
#         to_local_file_modified_time = os.path.getmtime(to_local_file)
#     else:
#         to_local_file_modified_time = 0

#     # Send a HEAD request to get last modified time of the remote file
#     response = requests.head(url)
#     if 'Last-Modified' in response.headers:
#         remote_file_modified_time = datetime.strptime(
#             response.headers['Last-Modified'], '%a, %d %b %Y %H:%M:%S GMT'
#         ).timestamp()
#     else:
#         # If the Last-Modified header not available, assume file needs to be downloaded
#         remote_file_modified_time = float('inf')

#     # Download the file if the remote file is newer
#     if force or remote_file_modified_time > to_local_file_modified_time:
#         response = requests.get(url)
#         with open(to_local_file, 'wb') as file:
#             file.write(response.content)
#             if verbose: print(f"Downloaded file from {url} -> {to_local_file}")
#     else:
#         if verbose: print("Local file is up to date.")
#     return to_local_file


########################################################################
def gunzip(filename: str) -> str:
    """Unzip a `.gz` file to a temporary file and returns its path."""
    if not filename.endswith(".gz"):
        # colors.printc("gunzip() error: file must end with .gz", c='r')
        return filename

    import gzip

    tmp_file = NamedTemporaryFile(delete=False)
    tmp_file.name = os.path.join(
        os.path.dirname(tmp_file.name), os.path.basename(filename).replace(".gz", "")
    )
    inF = gzip.open(filename, "rb")
    with open(tmp_file.name, "wb") as outF:
        outF.write(inF.read())
    inF.close()
    return tmp_file.name

########################################################################
def file_info(file_path: str) -> Tuple[str, str]:
    """Return the file size and creation time of input file"""
    siz, created = "", ""
    if os.path.isfile(file_path):
        f_info = os.stat(file_path)
        num = f_info.st_size
        for x in ["B", "KB", "MB", "GB", "TB"]:
            if num < 1024.0:
                break
            num /= 1024.0
        siz = "%3.1f%s" % (num, x)
        created = time.ctime(os.path.getmtime(file_path))
    return siz, created


###################################################################
def loadStructuredPoints(filename: Union[str, os.PathLike], as_points=True):
    """
    Load and return a `vtkStructuredPoints` object from file.

    If `as_points` is True, return a `Points` object
    instead of a `vtkStructuredPoints`.
    """
    filename = str(filename)
    reader = vtki.new("StructuredPointsReader")
    reader.SetFileName(filename)
    reader.Update()
    if as_points:
        v2p = vtki.new("ImageToPoints")
        v2p.SetInputData(reader.GetOutput())
        v2p.Update()
        pts = Points(v2p.GetOutput())
        return pts
    return reader.GetOutput()

########################################################################
def loadStructuredGrid(filename: Union[str, os.PathLike]):
    """Load and return a `vtkStructuredGrid` object from file."""
    filename = str(filename)
    if filename.endswith(".vts"):
        reader = vtki.new("XMLStructuredGridReader")
    else:
        reader = vtki.new("StructuredGridReader")
    reader.SetFileName(filename)
    reader.Update()
    return reader.GetOutput()


###################################################################
def load3DS(filename: Union[str, os.PathLike]) -> Assembly:
    """Load `3DS` file format from file."""
    filename = str(filename)
    renderer = vtki.vtkRenderer()
    renWin = vtki.vtkRenderWindow()
    renWin.AddRenderer(renderer)

    importer = vtki.new("3DSImporter")
    importer.SetFileName(filename)
    importer.ComputeNormalsOn()
    importer.SetRenderWindow(renWin)
    importer.Update()

    actors = renderer.GetActors()  # vtkActorCollection
    acts = []
    for i in range(actors.GetNumberOfItems()):
        a = actors.GetItemAsObject(i)
        acts.append(a)
    del renWin

    wrapped_acts = []
    for a in acts:
        try:
            newa = Mesh(a.GetMapper().GetInput())
            newa.actor = a
            wrapped_acts.append(newa)
            # print("loaded 3DS object", [a])
        except:
            print("ERROR: cannot load 3DS object part", [a])
    return vedo.Assembly(wrapped_acts)

########################################################################
def loadOFF(filename: Union[str, os.PathLike]) -> Mesh:
    """Read the OFF file format (polygonal mesh)."""
    filename = str(filename)
    with open(filename, "r", encoding="UTF-8") as f:
        lines = f.readlines()

    vertices = []
    faces = []
    NumberOfVertices = 0
    i = -1
    for text in lines:
        if len(text) == 0:
            continue
        if text == "\n":
            continue
        if "#" in text:
            continue
        if "OFF" in text:
            continue

        ts = text.split()
        n = len(ts)

        if not NumberOfVertices and n > 1:
            NumberOfVertices, NumberOfFaces = int(ts[0]), int(ts[1])
            continue
        i += 1

        if i < NumberOfVertices and n == 3:
            x, y, z = float(ts[0]), float(ts[1]), float(ts[2])
            vertices.append([x, y, z])

        ids = []
        if NumberOfVertices <= i < (NumberOfVertices + NumberOfFaces + 1) and n > 2:
            ids += [int(xx) for xx in ts[1:]]
            faces.append(ids)

    return Mesh(utils.buildPolyData(vertices, faces))

def loadSTEP(filename: Union[str, os.PathLike], deflection=1.0) -> Mesh:
    """
    Reads a 3D STEP file and returns its mesh representation as vertices and triangles.

    Parameters:
    - filename (str): Path to the STEP file.
    - deflection (float): Linear deflection for meshing accuracy (smaller values yield finer meshes).

    Returns:
    - vertices (list of tuples): List of (x, y, z) coordinates of the mesh vertices.
    - triangles (list of tuples): List of (i, j, k) indices representing the triangles.

    Raises:
    - Exception: If the STEP file cannot be read.
    """
    try:
        from OCC.Core.STEPControl import STEPControl_Reader  # type: ignore
        from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh  # type: ignore
        from OCC.Core.TopExp import TopExp_Explorer  # type: ignore
        from OCC.Core.TopoDS import topods  # type: ignore
        from OCC.Core.BRep import BRep_Tool  # type: ignore
        from OCC.Core.TopAbs import TopAbs_FACE  # type: ignore
        from OCC.Core.TopLoc import TopLoc_Location  # type: ignore
    except ImportError:
        raise ImportError(
            "OCC library not found.\n\nPlease install 'pythonocc-core'. "
            "You can install it using the following command:\n"
            "\t\tconda install -c conda-forge pythonocc-core"
        )

    # Initialize the STEP reader
    reader = STEPControl_Reader()
    status = reader.ReadFile(str(filename))
    if status != 1:  # Check if reading was successful (IFSelect_RetDone = 1)
        raise Exception("Error reading STEP file")

    # Transfer the STEP data into a shape
    reader.TransferRoots()
    shape = reader.OneShape()

    # Mesh the shape with the specified deflection
    mesh = BRepMesh_IncrementalMesh(shape, deflection)
    mesh.Perform()

    # Extract vertices and triangles
    explorer = TopExp_Explorer(shape, TopAbs_FACE)
    vertices = []
    triangles = []
    vertex_index = 0

    # Iterate over all faces in the shape
    while explorer.More():
        face = topods.Face(explorer.Current())
        location = TopLoc_Location()
        triangulation = BRep_Tool.Triangulation(face, location)

        if triangulation:
            # Extract vertices from the triangulation
            for i in range(1, triangulation.NbNodes() + 1):
                point = triangulation.Node(i).Transformed(location.Transformation())
                vertices.append((point.X(), point.Y(), point.Z()))

            # Extract triangles with adjusted indices
            for i in range(1, triangulation.NbTriangles() + 1):
                triangle = triangulation.Triangle(i)
                n1, n2, n3 = triangle.Get()  # 1-based indices
                triangles.append((
                    n1 + vertex_index - 1,
                    n2 + vertex_index - 1,
                    n3 + vertex_index - 1
                ))

            # Update the vertex index offset for the next face
            vertex_index += triangulation.NbNodes()

        explorer.Next()

    # Create a mesh object
    mesh = Mesh([vertices, triangles])
    return mesh

########################################################################
def loadGeoJSON(filename: Union[str, os.PathLike]) -> Mesh:
    """Load GeoJSON files."""
    filename = str(filename)
    jr = vtki.new("GeoJSONReader")
    jr.SetFileName(filename)
    jr.Update()
    return Mesh(jr.GetOutput())

########################################################################
def loadDolfin(filename: Union[str, os.PathLike]) -> Union[Mesh, "vedo.TetMesh", None]:
    """
    Reads a `Fenics/Dolfin` file format (.xml or .xdmf).

    Return a `Mesh` or a `TetMesh` object.
    """
    filename = str(filename)
    try:
        import dolfin
    except ImportError:
        vedo.logger.error("loadDolfin(): dolfin module not found. Install with:")
        vedo.logger.error("  conda create -n fenics -c conda-forge fenics")
        vedo.logger.error("  conda install conda-forge::mshr")
        vedo.logger.error("  conda activate fenics")
        return None

    if filename.lower().endswith(".xdmf"):
        f = dolfin.XDMFFile(filename)
        m = dolfin.Mesh()
        f.read(m)
    else:
        m = dolfin.Mesh(filename)

    cells = m.cells()
    verts = m.coordinates()

    if cells.size and verts.size:
        if len(cells[0]) == 4:  # tetrahedral mesh
            return vedo.TetMesh([verts, cells])
        elif len(cells[0]) == 3:  # triangular mesh
            return Mesh([verts, cells])

    return None


########################################################################
def loadPVD(filename: Union[str, os.PathLike]) -> Union[List[Any], None]:
    """Read paraview files."""
    filename = str(filename)
    import xml.etree.ElementTree as et

    tree = et.parse(filename)

    dname = os.path.dirname(filename)
    if not dname:
        dname = "."

    listofobjs = []
    for coll in tree.getroot():
        for dataset in coll:
            fname = dataset.get("file")
            if not fname:
                continue
            ob = load(dname + "/" + fname)
            tm = dataset.get("timestep")
            if tm:
                ob.time = tm
            listofobjs.append(ob)
    if len(listofobjs) == 1:
        return listofobjs[0]
    if len(listofobjs) == 0:
        return None
    return listofobjs

########################################################################
def loadNeutral(filename: Union[str, os.PathLike]) -> "vedo.TetMesh":
    """
    Reads a `Neutral` tetrahedral file format.

    Returns an `TetMesh` object.
    """
    filename = str(filename)
    with open(filename, "r", encoding="UTF-8") as f:
        lines = f.readlines()

    ncoords = int(lines[0])
    coords = []
    for i in range(1, ncoords + 1):
        x, y, z = lines[i].split()
        coords.append([float(x), float(y), float(z)])

    ntets = int(lines[ncoords + 1])
    idolf_tets = []
    for i in range(ncoords + 2, ncoords + ntets + 2):
        text = lines[i].split()
        v0, v1, v2, v3 = int(text[1])-1, int(text[2])-1, int(text[3])-1, int(text[4])-1
        idolf_tets.append([v0, v1, v2, v3])

    return vedo.TetMesh([coords, idolf_tets])

########################################################################
def loadGmesh(filename: Union[str, os.PathLike]) -> Mesh:
    """Reads a `gmesh` file format. Return an `Mesh` object."""
    filename = str(filename)
    with open(filename, "r", encoding="UTF-8") as f:
        lines = f.readlines()

    nnodes = 0
    index_nodes = 0
    for i, line in enumerate(lines):
        if "$Nodes" in line:
            index_nodes = i + 1
            nnodes = int(lines[index_nodes])
            break
    node_coords = []
    for i in range(index_nodes + 1, index_nodes + 1 + nnodes):
        cn = lines[i].split()
        node_coords.append([float(cn[1]), float(cn[2]), float(cn[3])])

    nelements = 0
    index_elements = 0
    for i, line in enumerate(lines):
        if "$Elements" in line:
            index_elements = i + 1
            nelements = int(lines[index_elements])
            break
    elements = []
    for i in range(index_elements + 1, index_elements + 1 + nelements):
        ele = lines[i].split()
        elements.append([int(ele[-3]), int(ele[-2]), int(ele[-1])])

    poly = utils.buildPolyData(node_coords, elements, index_offset=1)
    return Mesh(poly)

########################################################################
def loadPCD(filename: Union[str, os.PathLike]) -> Points:
    """Return a `Mesh` made of only vertex points
    from the `PointCloud` library file format.

    Returns an `Points` object.
    """
    filename = str(filename)
    with open(filename, "r", encoding="UTF-8") as f:
        lines = f.readlines()

    start = False
    pts = []
    N, expN = 0, 0
    for text in lines:
        if start:
            if N >= expN:
                break
            l = text.split()
            pts.append([float(l[0]), float(l[1]), float(l[2])])
            N += 1
        if not start and "POINTS" in text:
            expN = int(text.split()[1])
        if not start and "DATA ascii" in text:
            start = True
    if expN != N:
        vedo.logger.warning(f"Mismatch in PCD file {expN} != {len(pts)}")
    poly = utils.buildPolyData(pts)
    return Points(poly).point_size(4)

#########################################################################
def from_numpy(d: dict) -> Mesh:
    """Create a Mesh object from a dictionary."""
    # recreate a mesh from numpy arrays
    keys = d.keys()

    points = d["points"]
    cells = d["cells"] if "cells" in keys else None
    lines = d["lines"] if "lines" in keys else None

    msh = Mesh([points, cells, lines])

    if "pointdata" in keys and isinstance(d["pointdata"], dict):
        for arrname, arr in d["pointdata"].items():
            msh.pointdata[arrname] = arr
    if "celldata" in keys and isinstance(d["celldata"], dict):
        for arrname, arr in d["celldata"].items():
            msh.celldata[arrname] = arr
    if "metadata" in keys and isinstance(d["metadata"], dict):
        for arrname, arr in d["metadata"].items():
            msh.metadata[arrname] = arr

    prp = msh.properties
    prp.SetAmbient(d['ambient'])
    prp.SetDiffuse(d['diffuse'])
    prp.SetSpecular(d['specular'])
    prp.SetSpecularPower(d['specularpower'])
    prp.SetSpecularColor(d['specularcolor'])

    prp.SetInterpolation(0)
    # prp.SetInterpolation(d['shading'])

    prp.SetOpacity(d['alpha'])
    prp.SetRepresentation(d['representation'])
    prp.SetPointSize(d['pointsize'])
    if d['color'] is not None:
        msh.color(d['color'])
    if "lighting_is_on" in d.keys():
        prp.SetLighting(d['lighting_is_on'])
    # Must check keys for backwards compatibility:
    if "linecolor" in d.keys() and d['linecolor'] is not None:
        msh.linecolor(d['linecolor'])
    if "backcolor" in d.keys() and d['backcolor'] is not None:
        msh.backcolor(d['backcolor'])

    if d['linewidth'] is not None:
        msh.linewidth(d['linewidth'])
    if "edge_visibility" in d.keys():
        prp.SetEdgeVisibility(d['edge_visibility']) # new

    lut_list  = d["LUT"]
    lut_range = d["LUT_range"]
    ncols = len(lut_list)
    lut = vtki.vtkLookupTable()
    lut.SetNumberOfTableValues(ncols)
    lut.SetRange(lut_range)
    for i in range(ncols):
        r, g, b, a = lut_list[i]
        lut.SetTableValue(i, r, g, b, a)
    lut.Build()
    msh.mapper.SetLookupTable(lut)
    msh.mapper.SetScalarRange(lut_range)

    try: # NEW in vedo 5.0
        arname = d["array_name_to_color_by"]
        msh.mapper.SetArrayName(arname)
        msh.mapper.SetInterpolateScalarsBeforeMapping(
            d["interpolate_scalars_before_mapping"])
        msh.mapper.SetUseLookupTableScalarRange(
            d["use_lookup_table_scalar_range"])
        msh.mapper.SetScalarRange(d["scalar_range"])
        msh.mapper.SetScalarVisibility(d["scalar_visibility"])
        msh.mapper.SetScalarMode(d["scalar_mode"])
        msh.mapper.SetColorMode(d["color_mode"])
        if d["scalar_visibility"]:
            if d["scalar_mode"] == 1:
                msh.dataset.GetPointData().SetActiveScalars(arname)
            if d["scalar_mode"] == 2:
                msh.dataset.GetCellData().SetActiveScalars(arname)

        if "texture_array" in keys and d["texture_array"] is not None:
            # recreate a vtkTexture object from numpy arrays:
            t = vtki.vtkTexture()
            t.SetInterpolate(d["texture_interpolate"])
            t.SetRepeat(d["texture_repeat"])
            t.SetQuality(d["texture_quality"])
            t.SetColorMode(d["texture_color_mode"])
            t.SetMipmap(d["texture_mipmap"])
            t.SetBlendingMode(d["texture_blending_mode"])
            t.SetEdgeClamp(d["texture_edge_clamp"])
            t.SetBorderColor(d["texture_border_color"])
            msh.actor.SetTexture(t)
            tcarray = None
            for arrname in msh.pointdata.keys():
                if "Texture" in arrname or "TCoord" in arrname:
                    tcarray = arrname
                    break
            if tcarray is not None:
                t.SetInputData(vedo.Image(d["texture_array"]).dataset)
                msh.pointdata.select_texture_coords(tcarray)

        # print("color_mode", d["color_mode"])
        # print("scalar_mode", d["scalar_mode"])
        # print("scalar_range", d["scalar_range"])
        # print("scalar_visibility", d["scalar_visibility"])
        # print("array_name_to_color_by", arname)
    except KeyError:
        pass

    if "time" in keys: msh.time = d["time"]
    if "name" in keys: msh.name = d["name"]
    # if "info" in keys: msh.info = d["info"]
    if "filename" in keys: msh.filename = d["filename"]
    if "pickable" in keys: msh.pickable(d["pickable"])
    if "dragable" in keys: msh.draggable(d["dragable"])
    return msh

#############################################################################
def _import_npy(fileinput: Union[str, os.PathLike]) -> "vedo.Plotter":
    """Import a vedo scene from numpy format."""
    fileinput = str(fileinput)

    fileinput = download(fileinput, verbose=False, force=True)
    if fileinput.endswith(".npy"):
        data = np.load(fileinput, allow_pickle=True, encoding="latin1").flatten()[0]
    elif fileinput.endswith(".npz"):
        data = np.load(fileinput, allow_pickle=True)["vedo_scenes"][0]

    if "use_parallel_projection" in data.keys():
        vedo.settings.use_parallel_projection = data["use_parallel_projection"]
    if "use_polygon_offset" in data.keys():
        vedo.settings.use_polygon_offset = data["use_polygon_offset"]
    if "polygon_offset_factor" in data.keys():
        vedo.settings.polygon_offset_factor = data["polygon_offset_factor"]
    if "polygon_offset_units" in data.keys():
        vedo.settings.polygon_offset_units = data["polygon_offset_units"]
    if "interpolate_scalars_before_mapping" in data.keys():
        vedo.settings.interpolate_scalars_before_mapping = data["interpolate_scalars_before_mapping"]
    if "default_font" in data.keys():
        vedo.settings.default_font = data["default_font"]
    if "use_depth_peeling" in data.keys():
        vedo.settings.use_depth_peeling = data["use_depth_peeling"]

    axes = data.pop("axes", 4) # UNUSED
    title = data.pop("title", "")
    backgrcol  = data.pop("backgrcol", "white")
    backgrcol2 = data.pop("backgrcol2", None)
    cam = data.pop("camera", None)

    if data["shape"] != (1, 1):
        data["size"] = "auto"  # disable size

    plt = vedo.Plotter(
        size=data["size"],  # not necessarily a good idea to set it
        axes=axes,          # must be zero to avoid recreating the axes
        title=title,
        bg=backgrcol,
        bg2=backgrcol2,
    )

    if cam:
        if "pos" in cam.keys():
            plt.camera.SetPosition(cam["pos"])
        if "focalPoint" in cam.keys(): # obsolete
            plt.camera.SetFocalPoint(cam["focalPoint"])
        if "focal_point" in cam.keys():
            plt.camera.SetFocalPoint(cam["focal_point"])
        if "viewup" in cam.keys():
            plt.camera.SetViewUp(cam["viewup"])
        if "distance" in cam.keys():
            plt.camera.SetDistance(cam["distance"])
        if "clippingRange" in cam.keys(): # obsolete
            plt.camera.SetClippingRange(cam["clippingRange"])
        if "clipping_range" in cam.keys():
            plt.camera.SetClippingRange(cam["clipping_range"])
        if "parallel_scale" in cam.keys():
            plt.camera.SetParallelScale(cam["parallel_scale"])

    ##############################################
    objs = []
    for d in data["objects"]:
        ### Mesh
        if d['type'].lower() == 'mesh':
            obj = from_numpy(d)

        ### Assembly
        elif d['type'].lower() == 'assembly':
            assacts = []
            for ad in d["actors"]:
                assacts.append(from_numpy(ad))
            obj = Assembly(assacts)
            obj.SetScale(d["scale"])
            obj.SetPosition(d["position"])
            obj.SetOrientation(d["orientation"])
            obj.SetOrigin(d["origin"])

        ### Volume
        elif d['type'].lower() == 'volume':
            obj = Volume(d["array"])
            obj.spacing(d["spacing"])
            obj.origin(d["origin"])
            if "jittering" in d.keys(): obj.jittering(d["jittering"])
            obj.mode(d["mode"])
            obj.color(d["color"])
            obj.alpha(d["alpha"])
            obj.alpha_gradient(d["alphagrad"])

        ### TetMesh
        elif d['type'].lower() == 'tetmesh':
            raise NotImplementedError("TetMesh not supported yet")

        ### ScalarBar2D
        elif d['type'].lower() == 'scalarbar2d':
            raise NotImplementedError("ScalarBar2D not supported yet")

        ### Image
        elif d['type'].lower() == 'image':
            obj = Image(d["array"])
            obj.alpha(d["alpha"])
            obj.actor.SetScale(d["scale"])
            obj.actor.SetPosition(d["position"])
            obj.actor.SetOrientation(d["orientation"])
            obj.actor.SetOrigin(d["origin"])

        ### Text2D
        elif d['type'].lower() == 'text2d':
            obj = vedo.shapes.Text2D(d["text"], font=d["font"], c=d["color"])
            obj.pos(d["position"]).size(d["size"])
            obj.background(d["bgcol"], d["alpha"])
            if d["frame"]:
                obj.frame(d["bgcol"])

        else:
            obj = None
            # vedo.logger.warning(f"Cannot import object {d}")

        if obj:
            keys = d.keys()
            if "time" in keys: obj.time = d["time"]
            if "name" in keys: obj.name = d["name"]
            # if "info" in keys: obj.info = d["info"]
            if "filename" in keys: obj.filename = d["filename"]
            objs.append(obj)

    plt.add(objs)
    plt.resetcam = False
    return plt

###########################################################
def loadImageData(filename: Union[str, os.PathLike]) -> Union[vtki.vtkImageData, None]:
    """Read and return a `vtkImageData` object from file."""
    filename = str(filename)
    if ".ome.tif" in filename.lower():
        reader = vtki.new("OMETIFFReader")
        # print("GetOrientationType ", reader.GetOrientationType())
        reader.SetOrientationType(vedo.settings.tiff_orientation_type)
    elif ".tif" in filename.lower():
        reader = vtki.new("TIFFReader")
        # print("GetOrientationType ", reader.GetOrientationType())
        reader.SetOrientationType(vedo.settings.tiff_orientation_type)
    elif ".slc" in filename.lower():
        reader = vtki.new("SLCReader")
        if not reader.CanReadFile(filename):
            vedo.logger.error(f"sorry, bad SLC file {filename}")
            return None
    elif ".vti" in filename.lower():
        reader = vtki.new("XMLImageDataReader")
    elif ".mhd" in filename.lower():
        reader = vtki.new("MetaImageReader")
    elif ".dem" in filename.lower():
        reader = vtki.new("DEMReader")
    elif ".nii" in filename.lower():
        reader = vtki.new("NIFTIImageReader")
    elif ".nrrd" in filename.lower():
        reader = vtki.new("NrrdReader")
        if not reader.CanReadFile(filename):
            vedo.logger.error(f"sorry, bad NRRD file {filename}")
            return None
    else:
        vedo.logger.error(f"cannot read file {filename}")
        return None
    reader.SetFileName(filename)
    reader.Update()
    return reader.GetOutput()

###########################################################
def write(objct: Any, fileoutput: Union[str, os.PathLike], binary=True) -> Any:
    """
    Write object to file. Same as `save()`.

    Supported extensions are:

    - `vtk, vti, ply, obj, stl, byu, vtp, vti, mhd, xyz, xml, tif, png, bmp`
    """
    fileoutput = str(fileoutput)

    ###############################
    if isinstance(objct, Assembly):
        dd = to_numpy(objct)
        sdict = {"objects": [dd]}
        np.save(fileoutput, sdict)
        return objct

    ###############################
    obj = objct.dataset

    try:
        # check if obj is a Mesh.actor and has a transform
        M = objct.actor.GetMatrix()
        if M and not M.IsIdentity():
            obj = objct.apply_transform_from_actor()
            obj = objct.dataset
            vedo.logger.info(
                f"object '{objct.name}' "
                "was manually moved. Writing uses current position."
            )
    except:
        pass

    fr = fileoutput.lower()
    if fr.endswith(".vtk"):
        writer = vtki.new("DataSetWriter")
    elif fr.endswith(".ply"):
        writer = vtki.new("PLYWriter")
        writer.AddComment("PLY file generated by vedo")
        lut = objct.mapper.GetLookupTable()
        if lut:
            pscal = obj.GetPointData().GetScalars()
            if not pscal:
                pscal = obj.GetCellData().GetScalars()
            if pscal and pscal.GetName():
                writer.SetArrayName(pscal.GetName())
            writer.SetLookupTable(lut)
    elif fr.endswith(".stl"):
        writer = vtki.new("STLWriter")
    elif fr.endswith(".vtp"):
        writer = vtki.new("XMLPolyDataWriter")
    elif fr.endswith(".vtu"):
        writer = vtki.new("XMLUnstructuredGridWriter")
    elif fr.endswith(".xyz"):
        writer = vtki.new("SimplePointsWriter")
    elif fr.endswith(".facet"):
        writer = vtki.new("FacetWriter")
    elif fr.endswith(".vti"):
        writer = vtki.new("XMLImageDataWriter")
    elif fr.endswith(".vtr"):
        writer = vtki.new("XMLRectilinearGridWriter")
    elif fr.endswith(".vtm"):
        g = vtki.new("MultiBlockDataGroupFilter")
        for ob in objct:
            try:
                g.AddInputData(ob)
            except TypeError:
                vedo.logger.warning(f"cannot save object of type {type(ob)}")
        g.Update()
        mb = g.GetOutputDataObject(0)
        wri = vtki.new("vtkXMLMultiBlockDataWriter")
        wri.SetInputData(mb)
        wri.SetFileName(fileoutput)
        wri.Write()
        return objct
    elif fr.endswith(".mhd"):
        writer = vtki.new("MetaImageWriter")
    elif fr.endswith(".nii"):
        writer = vtki.new("NIFTIImageWriter")
    elif fr.endswith(".png"):
        writer = vtki.new("PNGWriter")
    elif fr.endswith(".jpg"):
        writer = vtki.new("JPEGWriter")
    elif fr.endswith(".bmp"):
        writer = vtki.new("BMPWriter")
    elif fr.endswith(".tif") or fr.endswith(".tiff"):
        writer = vtki.new("TIFFWriter")
        writer.SetFileDimensionality(len(obj.GetDimensions()))
    elif fr.endswith(".obj"):
        with open(fileoutput, "w", encoding="UTF-8") as outF:
            outF.write("# OBJ file format with ext .obj\n")
            outF.write("# File generated by vedo\n")

            for p in objct.vertices:
                outF.write("v {:.8g} {:.8g} {:.8g}\n".format(*p))

            ptxt = objct.dataset.GetPointData().GetTCoords()
            if ptxt:
                ntxt = utils.vtk2numpy(ptxt)
                for vt in ntxt:
                    outF.write("vt " + str(vt[0]) + " " + str(vt[1]) + " 0.0\n")

            if isinstance(objct, Mesh):
                for i, f in enumerate(objct.cells):
                    fs = ""
                    for fi in f:
                        if ptxt:
                            fs += f" {fi+1}/{fi+1}"
                        else:
                            fs += f" {fi+1}"
                    outF.write(f"f{fs}\n")

                for l in objct.lines:
                    ls = ""
                    for li in l:
                        ls += str(li + 1) + " "
                    outF.write(f"l {ls}\n")
        return objct

    elif fr.endswith(".off"):
        with open(fileoutput, "w", encoding="UTF-8") as outF:
            outF.write("OFF\n")
            outF.write(str(objct.npoints) + " " + str(objct.ncells) + " 0\n\n")
            for p in objct.vertices:
                outF.write(" ".join([str(i) for i in p]) + "\n")
            for c in objct.cells:
                outF.write(str(len(c)) + " " + " ".join([str(i) for i in c]) + "\n")
        return objct

    elif fr.endswith(".xml"):  # write tetrahedral dolfin xml
        vertices = objct.vertices.astype(str)
        faces = np.array(objct.cells).astype(str)
        ncoords = vertices.shape[0]
        with open(fileoutput, "w", encoding="UTF-8") as outF:
            outF.write('<?xml version="1.0" encoding="UTF-8"?>\n')
            outF.write('<dolfin xmlns:dolfin="http://www.fenicsproject.org">\n')

            if len(faces[0]) == 4:  # write tetrahedral mesh
                ntets = faces.shape[0]
                outF.write('  <mesh celltype="tetrahedron" dim="3">\n')
                outF.write('    <vertices size="' + str(ncoords) + '">\n')
                for i in range(ncoords):
                    x, y, z = vertices[i]
                    outF.write('      <vertex index="'+str(i)+'" x="'+x+'" y="'+y+'" z="'+z+'"/>\n')
                outF.write('    </vertices>\n')
                outF.write('    <cells size="' + str(ntets) + '">\n')
                for i in range(ntets):
                    v0, v1, v2, v3 = faces[i]
                    outF.write('     <tetrahedron index="'+str(i)
                               + '" v0="'+v0+'" v1="'+v1+'" v2="'+v2+'" v3="'+v3+'"/>\n')

            elif len(faces[0]) == 3:  # write triangle mesh
                ntri = faces.shape[0]
                outF.write('  <mesh celltype="triangle" dim="2">\n')
                outF.write('    <vertices size="' + str(ncoords) + '">\n')
                for i in range(ncoords):
                    x, y, _ = vertices[i]
                    outF.write('      <vertex index="'+str(i)+'" x="'+x+'" y="'+y+'"/>\n')
                outF.write('    </vertices>\n')
                outF.write('    <cells size="' + str(ntri) + '">\n')
                for i in range(ntri):
                    v0, v1, v2 = faces[i]
                    outF.write('     <triangle index="'+str(i)+'" v0="'+v0+'" v1="'+v1+'" v2="'+v2+'"/>\n')

            outF.write("    </cells>\n")
            outF.write("  </mesh>\n")
            outF.write("</dolfin>\n")
        return objct

    else:
        vedo.logger.error(f"Unknown format {fileoutput}, file not saved")
        return objct

    try:
        if binary:
            writer.SetFileTypeToBinary()
        else:
            writer.SetFileTypeToASCII()
    except AttributeError:
        pass

    try:
        writer.SetInputData(obj)
        writer.SetFileName(fileoutput)
        writer.Write()
    except:
        vedo.logger.error(f"could not save {fileoutput}")
    return objct

def save(obj: Any, fileoutput="out.png", binary=True) -> Any:
    """Save an object to file. Same as `write()`."""
    return write(obj, fileoutput, binary)

def read(obj: Any, unpack=True, force=False) -> Any:
    """Read an object from file. Same as `load()`."""
    return load(obj, unpack, force)

###############################################################################
def export_window(fileoutput: Union[str, os.PathLike], binary=False, plt=None) -> "vedo.Plotter":
    """
    Exporter which writes out the rendered scene into an HTML, X3D or Numpy file.

    Example:
        - [export_x3d.py](https://github.com/marcomusy/vedo/tree/master/examples/other/export_x3d.py)

        Check out the HTML generated webpage [here](https://vedo.embl.es/examples/embryo.html).

        <img src='https://user-images.githubusercontent.com/32848391/57160341-c6ffbd80-6de8-11e9-95ff-7215ce642bc5.jpg' width="600"/>

    .. note::
        the rendering window can also be exported to `numpy` file `scene.npz`
        by pressing `E` key at any moment during visualization.
    """
    fileoutput = str(fileoutput)
    if plt is None:
        plt = vedo.plotter_instance

    fr = fileoutput.lower()
    ####################################################################
    if fr.endswith(".npy") or fr.endswith(".npz"):
        _export_npy(plt, fileoutput)

    ####################################################################
    elif fr.endswith(".x3d"):
        # obj = plt.get_actors()
        # if plt.axes_instances:
        #     obj.append(plt.axes_instances[0])

        # for a in obj:
        #     if isinstance(a, Assembly):
        #         plt.remove(a)
        #         plt.add(a.unpack())

        plt.render()

        exporter = vtki.new("X3DExporter")
        exporter.SetBinary(binary)
        exporter.FastestOff()
        exporter.SetInput(plt.window)
        exporter.SetFileName(fileoutput)
        # exporter.WriteToOutputStringOn()
        exporter.Update()
        exporter.Write()

        wsize = plt.window.GetSize()
        x3d_html = _x3d_html_template.replace("~fileoutput", fileoutput)
        x3d_html = x3d_html.replace("~width",  str(wsize[0]))
        x3d_html = x3d_html.replace("~height", str(wsize[1]))
        with open(fileoutput.replace(".x3d", ".html"), "w", encoding="UTF-8") as outF:
            outF.write(x3d_html)

    ####################################################################
    elif fr.endswith(".html"):
        savebk = vedo.notebook_backend
        vedo.notebook_backend = "k3d"
        vedo.settings.default_backend = "k3d"
        # acts = plt.get_actors()
        plt = vedo.backends.get_notebook_backend(plt.objects)

        with open(fileoutput, "w", encoding="UTF-8") as fp:
            fp.write(plt.get_snapshot())

        vedo.notebook_backend = savebk
        vedo.settings.default_backend = savebk

    else:
        vedo.logger.error(f"export extension {fr.split('.')[-1]} is not supported")

    return plt

#########################################################################
def to_numpy(act: Any) -> dict:
    """Encode a vedo object to numpy format."""

    ########################################################
    def _fillcommon(obj, adict):
        adict["filename"] = obj.filename
        adict["name"] = obj.name
        adict["time"] = obj.time
        adict["rendered_at"] = obj.rendered_at
        try:
            adict["transform"] = obj.transform.matrix
        except AttributeError:
            adict["transform"] = np.eye(4)

    ####################################################################
    try:
        obj = act.retrieve_object()
    except AttributeError:
        obj = act

    adict = {}
    adict["type"] = "unknown"

    ######################################################## Points/Mesh
    if isinstance(obj, (Points, vedo.UnstructuredGrid)):
        adict["type"] = "Mesh"
        _fillcommon(obj, adict)

        if isinstance(obj, vedo.UnstructuredGrid):
            # adict["type"] = "UnstructuredGrid"
            # adict["cells"] = obj.cells_as_flat_array
            poly = obj._actor.GetMapper().GetInput()
            mapper = obj._actor.GetMapper()
        else:
            poly = obj.dataset
            mapper = obj.mapper

        adict["points"] = obj.vertices.astype(float)

        adict["cells"] = None
        if poly.GetNumberOfPolys():
            adict["cells"] = obj.cells_as_flat_array

        adict["lines"] = None
        if poly.GetNumberOfLines():
            adict["lines"] = obj.lines#_as_flat_array

        adict["pointdata"] = {}
        for iname in obj.pointdata.keys():
            if "normals" in iname.lower():
                continue
            adict["pointdata"][iname] = obj.pointdata[iname]

        adict["celldata"] = {}
        for iname in obj.celldata.keys():
            if "normals" in iname.lower():
                continue
            adict["celldata"][iname] = obj.celldata[iname]

        adict["metadata"] = {}
        for iname in obj.metadata.keys():
            adict["metadata"][iname] = obj.metadata[iname]

        # NEW in vedo 5.0
        adict["scalar_mode"] = mapper.GetScalarMode()
        adict["array_name_to_color_by"] = mapper.GetArrayName()
        adict["color_mode"] = mapper.GetColorMode()
        adict["interpolate_scalars_before_mapping"] = mapper.GetInterpolateScalarsBeforeMapping()
        adict["use_lookup_table_scalar_range"] = mapper.GetUseLookupTableScalarRange()
        adict["scalar_range"] = mapper.GetScalarRange()
        adict["scalar_visibility"] = mapper.GetScalarVisibility()
        adict["pickable"] = obj.actor.GetPickable()
        adict["dragable"] = obj.actor.GetDragable()

        # adict["color_map_colors"]  = mapper.GetColorMapColors()   #vtkUnsignedCharArray
        # adict["color_coordinates"] = mapper.GetColorCoordinates() #vtkFloatArray
        texmap = mapper.GetColorTextureMap()  #vtkImageData
        if texmap:
            adict["color_texture_map"] = vedo.Image(texmap).tonumpy()
            # print("color_texture_map", adict["color_texture_map"].shape)

        adict["texture_array"] = None
        texture = obj.actor.GetTexture()
        if texture:
            adict["texture_array"] = vedo.Image(texture.GetInput()).tonumpy()
            adict["texture_interpolate"] = texture.GetInterpolate()
            adict["texture_repeat"] = texture.GetRepeat()
            adict["texture_quality"] = texture.GetQuality()
            adict["texture_color_mode"] = texture.GetColorMode()
            adict["texture_mipmap"] = texture.GetMipmap()
            adict["texture_blending_mode"] = texture.GetBlendingMode()
            adict["texture_edge_clamp"] = texture.GetEdgeClamp()
            adict["texture_border_color"] = texture.GetBorderColor()
            # print("tonumpy: texture", obj.name, adict["texture_array"].shape)

        adict["LUT"] = None
        adict["LUT_range"] = None
        lut = mapper.GetLookupTable()
        if lut:
            nlut = lut.GetNumberOfTableValues()
            lutvals = []
            for i in range(nlut):
                v4 = lut.GetTableValue(i)  # (r, g, b, alpha)
                lutvals.append(v4)
            adict["LUT"] = np.array(lutvals, dtype=np.float32)
            adict["LUT_range"] = np.array(lut.GetRange())

        prp = obj.properties
        adict["alpha"] = prp.GetOpacity()
        adict["representation"] = prp.GetRepresentation()
        adict["pointsize"] = prp.GetPointSize()

        adict["linecolor"] = None
        adict["linewidth"] = None
        adict["edge_visibility"] = prp.GetEdgeVisibility() # new in vedo 5.0
        if prp.GetEdgeVisibility():
            adict["linewidth"] = prp.GetLineWidth()
            adict["linecolor"] = prp.GetEdgeColor()

        adict["ambient"] = prp.GetAmbient()
        adict["diffuse"] = prp.GetDiffuse()
        adict["specular"] = prp.GetSpecular()
        adict["specularpower"] = prp.GetSpecularPower()
        adict["specularcolor"] = prp.GetSpecularColor()
        adict["shading"] = prp.GetInterpolation()  # flat phong..:
        adict["color"] = prp.GetColor()
        adict["lighting_is_on"] = prp.GetLighting()
        adict["backcolor"] = None
        if obj.actor.GetBackfaceProperty():
            adict["backcolor"] = obj.actor.GetBackfaceProperty().GetColor()

    ######################################################## Volume
    elif isinstance(obj, Volume):
        adict["type"] = "Volume"
        _fillcommon(obj, adict)
        adict["array"] = obj.tonumpy()
        adict["mode"] = obj.mode()
        adict["spacing"] = obj.spacing()
        adict["origin"] = obj.origin()

        prp = obj.properties
        ctf = prp.GetRGBTransferFunction()
        otf = prp.GetScalarOpacity()
        gotf = prp.GetGradientOpacity()
        smin, smax = ctf.GetRange()
        xs = np.linspace(smin, smax, num=256, endpoint=True)
        cols, als, algrs = [], [], []
        for x in xs:
            cols.append(ctf.GetColor(x))
            als.append(otf.GetValue(x))
            if gotf:
                algrs.append(gotf.GetValue(x))
        adict["color"] = cols
        adict["alpha"] = als
        adict["alphagrad"] = algrs

    ######################################################## Image
    elif isinstance(obj, Image):
        adict["type"] = "Image"
        _fillcommon(obj, adict)
        adict["array"] = obj.tonumpy()
        adict["scale"] = obj.actor.GetScale()
        adict["position"] = obj.actor.GetPosition()
        adict["orientation"] = obj.actor.GetOrientation()
        adict['origin'] = obj.actor.GetOrigin()
        adict["alpha"] = obj.alpha()

    ######################################################## Text2D
    elif isinstance(obj, vedo.Text2D):
        adict["type"] = "Text2D"
        adict["rendered_at"] = obj.rendered_at
        adict["text"] = obj.text()
        adict["position"] = obj.GetPosition()
        adict["color"] = obj.properties.GetColor()
        adict["font"] = obj.fontname
        adict["size"] = obj.properties.GetFontSize() / 22.5
        adict["bgcol"] = obj.properties.GetBackgroundColor()
        adict["alpha"] = obj.properties.GetBackgroundOpacity()
        adict["frame"] = obj.properties.GetFrame()

    ######################################################## Assembly
    elif isinstance(obj, Assembly):
        adict["type"] = "Assembly"
        _fillcommon(obj, adict)
        adict["parts"] = []
        for a in obj.unpack():
            adict["parts"].append(to_numpy(a))

    else:
        # vedo.logger.warning(f"to_numpy: cannot export object of type {type(obj)}")
        pass

    return adict


#########################################################################
def _export_npy(plt, fileoutput="scene.npz") -> None:

    fileoutput = str(fileoutput)

    sdict = {}
    sdict["shape"] = plt.shape
    sdict["sharecam"] = plt.sharecam
    sdict["camera"] = dict(
        pos=plt.camera.GetPosition(),
        focal_point=plt.camera.GetFocalPoint(),
        viewup=plt.camera.GetViewUp(),
        distance=plt.camera.GetDistance(),
        clipping_range=plt.camera.GetClippingRange(),
        parallel_scale=plt.camera.GetParallelScale(),
    )
    sdict["position"] = plt.pos
    sdict["size"] = plt.size
    sdict["axes"] = 0
    sdict["title"] = plt.title
    sdict["backgrcol"] = colors.get_color(plt.renderer.GetBackground())
    sdict["backgrcol2"] = None
    if plt.renderer.GetGradientBackground():
        sdict["backgrcol2"] = plt.renderer.GetBackground2()
    sdict["use_depth_peeling"] = plt.renderer.GetUseDepthPeeling()
    sdict["use_parallel_projection"] = plt.camera.GetParallelProjection()
    sdict["default_font"] = vedo.settings.default_font

    sdict["objects"] = []

    actors = plt.get_actors(include_non_pickables=True)
    # this ^ also retrieves Actors2D
    allobjs = []
    for i, a in enumerate(actors):

        if not a.GetVisibility():
            continue

        try:
            ob = a.retrieve_object()
            # print("get_actors",[ob], ob.name)
            if isinstance(ob, Assembly):
                asse_scale = ob.GetScale()
                asse_pos = ob.GetPosition()
                asse_ori = ob.GetOrientation()
                asse_org = ob.GetOrigin()
                for elem in ob.unpack():
                    elem.name = f"ASSEMBLY{i}_{ob.name}_{elem.name}"
                    # elem.info.update({"assembly": ob.name}) # TODO
                    # elem.info.update({"assembly_scale": asse_scale})
                    # elem.info.update({"assembly_position": asse_pos})
                    # elem.info.update({"assembly_orientation": asse_ori})
                    # elem.info.update({"assembly_origin": asse_org})
                    elem.metadata["assembly"] = ob.name
                    elem.metadata["assembly_scale"] = asse_scale
                    elem.metadata["assembly_position"] = asse_pos
                    elem.metadata["assembly_orientation"] = asse_ori
                    elem.metadata["assembly_origin"] = asse_org
                    allobjs.append(elem)
            else:
                allobjs.append(ob)

        except AttributeError:
            # print()
            # vedo.logger.warning(f"Cannot retrieve object of type {type(a)}")
            pass

    for a in allobjs:
        # print("to_numpy(): dumping", [a], a.name)
        # try:
        npobj = to_numpy(a)
        sdict["objects"].append(npobj)
        # except AttributeError:
        #     vedo.logger.warning(f"Cannot export object of type {type(a)}")

    if fileoutput.endswith(".npz"):
        np.savez_compressed(fileoutput, vedo_scenes=[sdict])
    else:
        np.save(fileoutput, [sdict])


########################################################################
def import_window(fileinput: Union[str, os.PathLike]) -> Union["vedo.Plotter", None]:
    """
    Import a whole scene from a Numpy NPZ file.

    Returns:
        `vedo.Plotter` instance
    """
    fileinput = str(fileinput)

    if fileinput.endswith(".npy") or fileinput.endswith(".npz"):
        return _import_npy(fileinput)

    # elif ".obj" in fileinput.lower():
    #     meshes = load_obj(fileinput, mtl_file, texture_path)
    #     plt = vedo.Plotter()
    #     plt.add(meshes)
    #     return plt

    # elif fileinput.endswith(".h5") or fileinput.endswith(".hdf5"):
    #     return _import_hdf5(fileinput) # in store/file_io_HDF5.py

    return None


def load_obj(fileinput: Union[str, os.PathLike], mtl_file=None, texture_path=None) -> List[Mesh]:
    """
    Import a set of meshes from a OBJ wavefront file.

    Arguments:
        mtl_file : (str)
            MTL file for OBJ wavefront files
        texture_path : (str)
            path of the texture files directory

    Returns:
        `list(Mesh)`
    """
    fileinput = str(fileinput)

    window = vtki.vtkRenderWindow()
    window.SetOffScreenRendering(1)
    renderer = vtki.vtkRenderer()
    window.AddRenderer(renderer)

    importer = vtki.new("OBJImporter")
    importer.SetFileName(fileinput)
    if mtl_file is None:
        mtl_file = fileinput.replace(".obj", ".mtl").replace(".OBJ", ".MTL")
    if os.path.isfile(mtl_file):
        importer.SetFileNameMTL(mtl_file)
    if texture_path is None:
        texture_path = fileinput.replace(".obj", ".txt").replace(".OBJ", ".TXT")
    # since the texture_path may be a directory which contains textures
    if os.path.exists(texture_path):
        importer.SetTexturePath(texture_path)
    importer.SetRenderWindow(window)
    importer.Update()

    actors = renderer.GetActors()
    actors.InitTraversal()
    objs = []
    for _ in range(actors.GetNumberOfItems()):
        vactor = actors.GetNextActor()
        msh = Mesh(vactor)
        msh.name = "OBJMesh"
        msh.copy_properties_from(vactor)
        tx = vactor.GetTexture()
        if tx:
            msh.texture(tx)
        objs.append(msh)
    return objs


##########################################################
def screenshot(filename="screenshot.png", scale=1, asarray=False) -> Union["vedo.Plotter", np.ndarray, None]:
    """
    Save a screenshot of the current rendering window.

    Alternatively, press key `Shift-S` in the rendering window to save a screenshot.
    You can also use keyword `screenshot` in `show(..., screenshot="pic.png")`.

    Arguments:
        scale : (int)
            Set image magnification as an integer multiplicative factor.
            E.g. setting a magnification of 2 produces an image twice as large,
            but 10x slower to generate.
        asarray : (bool)
            Return a numpy array of the image
    """
    filename = str(filename)
    # print("calling screenshot", filename, scale, asarray)

    if not vedo.plotter_instance or not vedo.plotter_instance.window:
        # vedo.logger.error("in screenshot(), rendering window is not present, skip.")
        return vedo.plotter_instance  ##########

    if vedo.plotter_instance.renderer:
        vedo.plotter_instance.renderer.ResetCameraClippingRange()

    if asarray and scale == 1 and not vedo.plotter_instance.offscreen:
        nx, ny = vedo.plotter_instance.window.GetSize()
        arr = vtki.vtkUnsignedCharArray()
        vedo.plotter_instance.window.GetRGBACharPixelData(0, 0, nx-1, ny-1, 0, arr)
        narr = vedo.vtk2numpy(arr).T[:3].T.reshape([ny, nx, 3])
        narr = np.flip(narr, axis=0)
        return narr  ##########

    ###########################
    if filename.endswith(".pdf"):
        writer = vtki.new("GL2PSExporter")
        writer.SetRenderWindow(vedo.plotter_instance.window)
        writer.Write3DPropsAsRasterImageOff()
        writer.SilentOn()
        writer.SetSortToBSP()
        writer.SetFileFormatToPDF()
        writer.SetFilePrefix(filename.replace(".pdf", ""))
        writer.Write()
        return vedo.plotter_instance  ##########

    elif filename.endswith(".svg"):
        writer = vtki.new("GL2PSExporter")
        writer.SetRenderWindow(vedo.plotter_instance.window)
        writer.Write3DPropsAsRasterImageOff()
        writer.SilentOn()
        writer.SetSortToBSP()
        writer.SetFileFormatToSVG()
        writer.SetFilePrefix(filename.replace(".svg", ""))
        writer.Write()
        return vedo.plotter_instance  ##########

    elif filename.endswith(".eps"):
        writer = vtki.new("GL2PSExporter")
        writer.SetRenderWindow(vedo.plotter_instance.window)
        writer.Write3DPropsAsRasterImageOff()
        writer.SilentOn()
        writer.SetSortToBSP()
        writer.SetFileFormatToEPS()
        writer.SetFilePrefix(filename.replace(".eps", ""))
        writer.Write()
        return vedo.plotter_instance  ##########

    if settings.screeshot_large_image:
        w2if = vtki.new("RenderLargeImage")
        w2if.SetInput(vedo.plotter_instance.renderer)
        w2if.SetMagnification(scale)
    else:
        w2if = vtki.new("WindowToImageFilter")
        w2if.SetInput(vedo.plotter_instance.window)
        if hasattr(w2if, "SetScale"):
            w2if.SetScale(int(scale), int(scale))
        if settings.screenshot_transparent_background:
            w2if.SetInputBufferTypeToRGBA()
        w2if.ReadFrontBufferOff()  # read from the back buffer
    w2if.Update()

    if asarray:
        pd = w2if.GetOutput().GetPointData()
        npdata = utils.vtk2numpy(pd.GetArray("ImageScalars"))
        # npdata = npdata[:, [0, 1, 2]]  # remove alpha channel, issue #1199
        ydim, xdim, _ = w2if.GetOutput().GetDimensions()
        npdata = npdata.reshape([xdim, ydim, -1])
        npdata = np.flip(npdata, axis=0)
        return npdata ###########################

    if filename.lower().endswith(".png"):
        writer = vtki.new("PNGWriter")
        writer.SetFileName(filename)
        writer.SetInputData(w2if.GetOutput())
        writer.Write()
    elif filename.lower().endswith(".jpg") or filename.lower().endswith(".jpeg"):
        writer = vtki.new("JPEGWriter")
        writer.SetFileName(filename)
        writer.SetInputData(w2if.GetOutput())
        writer.Write()
    else:  # add .png
        writer = vtki.new("PNGWriter")
        writer.SetFileName(filename + ".png")
        writer.SetInputData(w2if.GetOutput())
        writer.Write()
    return vedo.plotter_instance


def ask(*question, **kwarg) -> str:
    """
    Ask a question from command line. Return the answer as a string.
    See function `colors.printc()` for the description of the keyword options.

    Arguments:
        options : (list)
            a python list of possible answers to choose from.
        default : (str)
            the default answer when just hitting return.

    Example:
    ```python
    import vedo
    res = vedo.ask("Continue?", options=['Y','n'], default='Y', c='g')
    print(res)
    ```
    """
    kwarg.update({"end": " "})
    if "invert" not in kwarg:
        kwarg.update({"invert": True})
    if "box" in kwarg:
        kwarg.update({"box": ""})

    options = kwarg.pop("options", [])
    default = kwarg.pop("default", "")
    if options:
        opt = "["
        for o in options:
            opt += o + "/"
        opt = opt[:-1] + "]"
        colors.printc(*question, opt, **kwarg)
    else:
        colors.printc(*question, **kwarg)

    try:
        resp = input()
    except Exception:
        resp = ""
        return resp

    if options:
        if resp not in options:
            if default and str(repr(resp)) == "''":
                return default
            colors.printc("Please choose one option in:", opt, italic=True, bold=False)
            kwarg["options"] = options
            return ask(*question, **kwarg)  # ask again
    return resp


##############################################################################################
class Video:
    """
    Generate a video from a rendering window.
    """

    def __init__(self, name="movie.mp4", duration=None, fps=24, scale=1, backend="imageio"):
        """
        Class to generate a video from the specified rendering window.
        Program `ffmpeg` is used to create video from each generated frame.

        Arguments:
            name : (Union[str, os.PathLike])
                name of the output file.
            duration : (float)
                set the total `duration` of the video and recalculates `fps` accordingly.
            fps : (int)
                set the number of frames per second.
            scale : (int)
                set the image magnification as an integer multiplicative factor.
            backend : (str)
                the backend engine to be used `['imageio', 'ffmpeg', 'cv']`

        Examples:
            - [make_video.py](https://github.com/marcomusy/vedo/tree/master/examples/other/make_video.py)

            ![](https://user-images.githubusercontent.com/32848391/50739007-2bfc2b80-11da-11e9-97e6-620a3541a6fa.jpg)
        """
        self.name = str(name)
        self.duration = duration
        self.backend = backend
        self.fps = float(fps)
        self.command = "ffmpeg -loglevel panic -y -r"
        self.options = "-b:v 8000k"
        self.scale = scale

        self.frames = []
        self.tmp_dir = TemporaryDirectory()
        self.get_filename = lambda x: os.path.join(self.tmp_dir.name, x)
        colors.printc(":video:  Video file", self.name, "is open... ", c="m", end="")

    def add_frame(self) -> "Video":
        """Add frame to current video."""
        fr = self.get_filename(str(len(self.frames)) + ".png")
        screenshot(fr, scale=self.scale)
        self.frames.append(fr)
        return self

    def pause(self, pause=0) -> "Video":
        """Insert a `pause`, in seconds."""
        fr = self.frames[-1]
        n = int(self.fps * pause)
        for _ in range(n):
            fr2 = self.get_filename(str(len(self.frames)) + ".png")
            self.frames.append(fr2)
            os.system("cp -f %s %s" % (fr, fr2))
        return self

    def action(self, elevation=(0, 80), azimuth=(0, 359), cameras=(), resetcam=False) -> "Video":
        """
        Automatic shooting of a static scene by specifying rotation and elevation ranges.

        Arguments:
            elevation : list
                initial and final elevation angles
            azimuth_range : list
                initial and final azimuth angles
            cameras : list
                list of cameras to go through, each camera can be dictionary or a vtkCamera
        """
        if not self.duration:
            self.duration = 5

        plt = vedo.plotter_instance
        if not plt:
            vedo.logger.error("No vedo plotter found, cannot make video.")
            return self
        n = int(self.fps * self.duration)

        cams = []
        for cm in cameras:
            cams.append(utils.camera_from_dict(cm))
        nc = len(cams)

        plt.show(resetcam=resetcam, interactive=False)

        if nc:
            for i in range(n):
                plt.move_camera(cams, i / (n-1))
                plt.render()
                self.add_frame()

        else:  ########################################

            for i in range(n):
                plt.camera.Elevation((elevation[1] - elevation[0]) / n)
                plt.camera.Azimuth((azimuth[1] - azimuth[0]) / n)
                plt.render()
                self.add_frame()

        return self

    def close(self) -> None:
        """
        Render the video and write it to file.
        """
        if self.duration:
            self.fps = int(len(self.frames) / float(self.duration) + 0.5)
            colors.printc("recalculated fps:", self.fps, c="m", end="")
        else:
            self.fps = int(self.fps)

        ########################################
        if self.backend == "ffmpeg":
            out = os.system(
                self.command
                + " "
                + str(self.fps)
                + " -i "
                + f"'{self.tmp_dir.name}'"
                + os.sep
                + "%01d.png "
                + self.options
                + " "
                + f"'{self.name}'"
            )
            if out:
                vedo.logger.error(f":noentry: backend {self.backend} returning error: {out}")
            else:
                colors.printc(f":save: saved to {self.name}", c="m")

        ########################################
        elif "cv" in self.backend:
            try:
                import cv2  # type: ignore
            except ImportError:
                vedo.logger.error("opencv is not installed")
                return

            cap = cv2.VideoCapture(os.path.join(self.tmp_dir.name, "%1d.png"))
            fourcc = cv2.VideoWriter_fourcc(*"mp4v")
            if vedo.plotter_instance:
                w, h = vedo.plotter_instance.window.GetSize()
                writer = cv2.VideoWriter(self.name, fourcc, self.fps, (w, h), True)
            else:
                vedo.logger.error("No vedo plotter found, cannot make video.")
                return

            while True:
                ret, frame = cap.read()
                if not ret:
                    break
                writer.write(frame)

            cap.release()
            writer.release()

        ########################################
        elif "imageio" in self.backend:
            try:
                import imageio
            except ImportError:
                vedo.logger.error("Please install imageio with:\n pip install imageio[ffmpeg]")
                return

            if self.name.endswith(".mp4"):
                writer = imageio.get_writer(self.name, fps=self.fps)
            elif self.name.endswith(".gif"):
                writer = imageio.get_writer(self.name, mode="I", duration=1 / self.fps)
            elif self.name.endswith(".webm"):
                writer = imageio.get_writer(self.name, format="webm", fps=self.fps)
            else:
                vedo.logger.error(f"Unknown format of {self.name}.")
                return

            for f in utils.humansort(self.frames):
                image = imageio.v3.imread(f)
                try:
                    writer.append_data(image)
                except TypeError:
                    vedo.logger.error(f"Could not append data to video {self.name}")
                    vedo.logger.error("Please install imageio with: pip install imageio[ffmpeg]")
                    break
            try:
                writer.close()
                colors.printc(f"... saved as {self.name}", c="m")
            except:
                colors.printc(f":noentry: Could not save video {self.name}", c="r")

        # finalize cleanup
        self.tmp_dir.cleanup()

    def split_frames(self, output_dir="video_frames", prefix="frame_", file_format="png") -> None:
        """Split an existing video file into frames."""
        try:
            import imageio
        except ImportError:
            vedo.logger.error("\nPlease install imageio with:\n pip install imageio")
            return

        # Create the output directory if it doesn't exist
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)

        # Create a reader object to read the video
        reader = imageio.get_reader(self.name)

        # Loop through each frame of the video and save it as image
        print()
        for i, frame in utils.progressbar(
            enumerate(reader), title=f"writing {file_format} frames", c="m", width=20
        ):
            output_file = os.path.join(output_dir, f"{prefix}{str(i).zfill(5)}.{format}")
            imageio.imwrite(output_file, frame, format=file_format)
