Repository: yt-project/yt

Issue:
Volume rendering saving to other figure format
### Bug report

**Bug summary**

The current scene.save for volume rendering doesn't recognize the correct file format suffixes and still saves the figure only to .png file instead. 
**Code for reproduction**

```

import yt

ds = yt.load('IsolatedGalaxy/galaxy0030/galaxy0030')

sc = yt.create_scene(ds,'density', lens_type='perspective')

sc.save('rendering.pdf', sigma_clip=6.0)

```

**Actual outcome**

The figure saves to a file called "rendering.pdf.png"

**Expected outcome**

It saves to "rendering.pdf"

**Version Information**
<!--Please specify your platform and versions of the relevant libraries you are
using:-->
  * Operating System:  SUSE Linux Enterprise Server 12
  * Python Version:  3.6
  * yt version: 3.4-dev (dependencies installed using conda default channel, but yt built from source, the source code is updated with the master branch on git)
 * Other Libraries (if applicable): N/A





Discussion:

Resolving patch:
diff --git a/yt/visualization/volume_rendering/scene.py b/yt/visualization/volume_rendering/scene.py
--- a/yt/visualization/volume_rendering/scene.py
+++ b/yt/visualization/volume_rendering/scene.py
@@ -241,8 +241,10 @@ def save(self, fname=None, sigma_clip=None):
         Parameters
         ----------
         fname: string, optional
-            If specified, save the rendering as a bitmap to the file "fname".
+            If specified, save the rendering as to the file "fname".
             If unspecified, it creates a default based on the dataset filename.
+            The file format is inferred from the filename's suffix. Supported
+            fomats are png, pdf, eps, and ps.
             Default: None
         sigma_clip: float, optional
             Image values greater than this number times the standard deviation
@@ -302,8 +304,38 @@ def save(self, fname=None, sigma_clip=None):
         self.render()
 
         mylog.info("Saving render %s", fname)
-        self._last_render.write_png(fname, sigma_clip=sigma_clip)
-
+        # We can render pngs natively but for other formats we defer to
+        # matplotlib.
+        if suffix == '.png':
+            self._last_render.write_png(fname, sigma_clip=sigma_clip)
+        else:
+            from matplotlib.figure import Figure
+            from matplotlib.backends.backend_pdf import \
+                FigureCanvasPdf
+            from matplotlib.backends.backend_ps import \
+                FigureCanvasPS
+            shape = self._last_render.shape
+            fig = Figure((shape[0]/100., shape[1]/100.))
+            if suffix == '.pdf':
+                canvas = FigureCanvasPdf(fig)
+            elif suffix in ('.eps', '.ps'):
+                canvas = FigureCanvasPS(fig)
+            else:
+                raise NotImplementedError(
+                    "Unknown file suffix '{}'".format(suffix))
+            ax = fig.add_axes([0, 0, 1, 1])
+            ax.set_axis_off()
+            out = self._last_render
+            nz = out[:, :, :3][out[:, :, :3].nonzero()]
+            max_val = nz.mean() + sigma_clip * nz.std()
+            alpha = 255 * out[:, :, 3].astype('uint8')
+            out = np.clip(out[:, :, :3] / max_val, 0.0, 1.0) * 255
+            out = np.concatenate(
+                [out.astype('uint8'), alpha[..., None]], axis=-1)
+            # not sure why we need rot90, but this makes the orentation
+            # match the png writer
+            ax.imshow(np.rot90(out), origin='lower')
+            canvas.print_figure(fname, dpi=100)
 
     def save_annotated(self, fname=None, label_fmt=None,
                        text_annotate=None, dpi=100, sigma_clip=None):


Test patch:
diff --git a/yt/testing.py b/yt/testing.py
--- a/yt/testing.py
+++ b/yt/testing.py
@@ -1019,3 +1019,30 @@ def assert_allclose_units(actual, desired, rtol=1e-7, atol=0, **kwargs):
     at = at.value
 
     return assert_allclose(act, des, rt, at, **kwargs)
+
+def assert_fname(fname):
+    """Function that checks file type using libmagic"""
+    if fname is None:
+        return
+
+    with open(fname, 'rb') as fimg:
+        data = fimg.read()
+    image_type = ''
+
+    # see http://www.w3.org/TR/PNG/#5PNG-file-signature
+    if data.startswith(b'\211PNG\r\n\032\n'):
+        image_type = '.png'
+    # see http://www.mathguide.de/info/tools/media-types/image/jpeg
+    elif data.startswith(b'\377\330'):
+        image_type = '.jpeg'
+    elif data.startswith(b'%!PS-Adobe'):
+        data_str = data.decode("utf-8", "ignore")
+        if 'EPSF' in data_str[:data_str.index('\n')]:
+            image_type = '.eps'
+        else:
+            image_type = '.ps'
+    elif data.startswith(b'%PDF'):
+        image_type = '.pdf'
+
+    return image_type == os.path.splitext(fname)[1]
+
diff --git a/yt/visualization/tests/test_callbacks.py b/yt/visualization/tests/test_callbacks.py
--- a/yt/visualization/tests/test_callbacks.py
+++ b/yt/visualization/tests/test_callbacks.py
@@ -23,9 +23,9 @@
 from yt.testing import \
     fake_amr_ds, \
     fake_tetrahedral_ds, \
-    fake_hexahedral_ds
+    fake_hexahedral_ds, \
+    assert_fname
 import yt.units as u
-from .test_plotwindow import assert_fname
 from yt.utilities.exceptions import \
     YTPlotCallbackError, \
     YTDataTypeUnsupported
diff --git a/yt/visualization/tests/test_particle_plot.py b/yt/visualization/tests/test_particle_plot.py
--- a/yt/visualization/tests/test_particle_plot.py
+++ b/yt/visualization/tests/test_particle_plot.py
@@ -18,10 +18,12 @@
 import unittest
 from yt.data_objects.profiles import create_profile
 from yt.visualization.tests.test_plotwindow import \
-    assert_fname, WIDTH_SPECS, ATTR_ARGS
+    WIDTH_SPECS, ATTR_ARGS
 from yt.data_objects.particle_filters import add_particle_filter
 from yt.testing import \
-    fake_particle_ds, assert_array_almost_equal
+    fake_particle_ds, \
+    assert_array_almost_equal, \
+    assert_fname
 from yt.utilities.answer_testing.framework import \
     requires_ds, \
     data_dir_load, \
diff --git a/yt/visualization/tests/test_plotwindow.py b/yt/visualization/tests/test_plotwindow.py
--- a/yt/visualization/tests/test_plotwindow.py
+++ b/yt/visualization/tests/test_plotwindow.py
@@ -24,7 +24,7 @@
 
 from yt.testing import \
     fake_random_ds, assert_equal, assert_rel_equal, assert_array_equal, \
-    assert_array_almost_equal, assert_raises
+    assert_array_almost_equal, assert_raises, assert_fname
 from yt.utilities.answer_testing.framework import \
     requires_ds, data_dir_load, PlotWindowAttributeTest
 from yt.utilities.exceptions import \
@@ -43,33 +43,6 @@ def setup():
     ytcfg["yt", "__withintesting"] = "True"
 
 
-def assert_fname(fname):
-    """Function that checks file type using libmagic"""
-    if fname is None:
-        return
-
-    with open(fname, 'rb') as fimg:
-        data = fimg.read()
-    image_type = ''
-
-    # see http://www.w3.org/TR/PNG/#5PNG-file-signature
-    if data.startswith(b'\211PNG\r\n\032\n'):
-        image_type = '.png'
-    # see http://www.mathguide.de/info/tools/media-types/image/jpeg
-    elif data.startswith(b'\377\330'):
-        image_type = '.jpeg'
-    elif data.startswith(b'%!PS-Adobe'):
-        data_str = data.decode("utf-8", "ignore")
-        if 'EPSF' in data_str[:data_str.index('\n')]:
-            image_type = '.eps'
-        else:
-            image_type = '.ps'
-    elif data.startswith(b'%PDF'):
-        image_type = '.pdf'
-
-    return image_type == os.path.splitext(fname)[1]
-
-
 TEST_FLNMS = [None, 'test', 'test.png', 'test.eps',
               'test.ps', 'test.pdf']
 M7 = "DD0010/moving7_0010"
diff --git a/yt/visualization/tests/test_profile_plots.py b/yt/visualization/tests/test_profile_plots.py
--- a/yt/visualization/tests/test_profile_plots.py
+++ b/yt/visualization/tests/test_profile_plots.py
@@ -21,11 +21,12 @@
 from yt.testing import \
     fake_random_ds, \
     assert_array_almost_equal, \
-    requires_file
+    requires_file, \
+    assert_fname
 from yt.visualization.profile_plotter import \
     ProfilePlot, PhasePlot
 from yt.visualization.tests.test_plotwindow import \
-    assert_fname, TEST_FLNMS
+    TEST_FLNMS
 from yt.utilities.answer_testing.framework import \
     PhasePlotAttributeTest, \
     requires_ds, \
diff --git a/yt/visualization/volume_rendering/tests/test_scene.py b/yt/visualization/volume_rendering/tests/test_scene.py
--- a/yt/visualization/volume_rendering/tests/test_scene.py
+++ b/yt/visualization/volume_rendering/tests/test_scene.py
@@ -16,7 +16,9 @@
 import os
 import tempfile
 import shutil
-from yt.testing import fake_random_ds
+from yt.testing import \
+    fake_random_ds, \
+    assert_fname
 from yt.visualization.volume_rendering.api import volume_render, VolumeSource
 import numpy as np
 from unittest import TestCase
@@ -78,7 +80,10 @@ def test_rotation(self):
         ma_bound = ((ma-mi)*(0.90))+mi
         tf.map_to_colormap(mi_bound, ma_bound,  scale=0.01, colormap='Reds_r')
         sc.render()
-        sc.save('test_scene.png', sigma_clip=6.0)
+        for suffix in ['png', 'eps', 'ps', 'pdf']:
+            fname = 'test_scene.{}'.format(suffix)
+            sc.save(fname, sigma_clip=6.0)
+            assert_fname(fname)
 
         nrot = 2 
         for i in range(nrot):
diff --git a/yt/visualization/volume_rendering/tests/test_vr_cameras.py b/yt/visualization/volume_rendering/tests/test_vr_cameras.py
--- a/yt/visualization/volume_rendering/tests/test_vr_cameras.py
+++ b/yt/visualization/volume_rendering/tests/test_vr_cameras.py
@@ -18,14 +18,14 @@
 import tempfile
 import shutil
 from yt.testing import \
-    fake_random_ds
+    fake_random_ds, \
+    assert_fname
 import numpy as np
 from yt.visualization.volume_rendering.old_camera import \
     PerspectiveCamera, StereoPairCamera, InteractiveCamera, ProjectionCamera, \
     FisheyeCamera
 from yt.visualization.volume_rendering.api import ColorTransferFunction, \
     ProjectionTransferFunction
-from yt.visualization.tests.test_plotwindow import assert_fname
 from unittest import TestCase