Repository: vega/altair

Issue:
chart.save does not support pdf as shown in docs
[The docs](https://altair-viz.github.io/user_guide/saving_charts.html#png-svg-and-pdf-format) for saving charts show `chart.save('chart.pdf')` as one of the options, but running that results in `ValueError: unrecognized format: 'pdf'`, even though `altair_saver` is installed and `from altair_saver import save; save(chart, "chart.pdf")` works.

By contrast, `chart.save("chart.svg")` works fine.

Looking at the [docstring of `Chart.save`](https://altair-viz.github.io/user_guide/generated/toplevel/altair.Chart.html#altair.Chart.save), it looks like pdf isn't even supposed to work: "Supported formats are json, html, png, svg". But maybe it's just a matter of wiring it up?

If the issue is more complicated than that (maybe because `Chart.save` is currently hardwired to use the Selenium backend, which doesn't offer pdf export?), then I would suggest just removing `chart.save('chart.pdf')` from the code example and refer users who are interested in pdf export functionality to `altair_saver`'s docs :)


Discussion:
This requires altair version 4.1 or later. Check what version you are using with
```python
import altair
print(altair.__version__)
```
`4.1.0`, I've just installed it... But maybe I've misconfigured it then?
Ah, yeah, you're right. the mimebundle code has been updated, but we neglected to thread through the save method for pdf. This is a bug.
What error do you get when trying to save a png? I'm surprised that doesn't work, because (unlike PDF) it's covered in the test suite in several places.

Resolving patch:
diff --git a/altair/utils/save.py b/altair/utils/save.py
--- a/altair/utils/save.py
+++ b/altair/utils/save.py
@@ -73,7 +73,7 @@ def save(
             format = fp.split(".")[-1]
         else:
             raise ValueError(
-                "must specify file format: " "['png', 'svg', 'html', 'json']"
+                "must specify file format: " "['png', 'svg', 'pdf', 'html', 'json']"
             )
 
     spec = chart.to_dict()
@@ -108,7 +108,7 @@ def save(
             **kwargs,
         )
         write_file_or_filename(fp, mimebundle["text/html"], mode="w")
-    elif format in ["png", "svg"]:
+    elif format in ["png", "svg", "pdf"]:
         mimebundle = spec_to_mimebundle(
             spec=spec,
             format=format,
@@ -122,6 +122,8 @@ def save(
         )
         if format == "png":
             write_file_or_filename(fp, mimebundle["image/png"], mode="wb")
+        elif format == "pdf":
+            write_file_or_filename(fp, mimebundle["application/pdf"], mode="wb")
         else:
             write_file_or_filename(fp, mimebundle["image/svg+xml"], mode="w")
     else:
diff --git a/altair/vegalite/v3/api.py b/altair/vegalite/v3/api.py
--- a/altair/vegalite/v3/api.py
+++ b/altair/vegalite/v3/api.py
@@ -455,7 +455,8 @@ def save(
     ):
         """Save a chart to file in a variety of formats
 
-        Supported formats are json, html, png, svg
+        Supported formats are json, html, png, svg, pdf; the last three require
+        the altair_saver package to be installed.
 
         Parameters
         ----------
diff --git a/altair/vegalite/v4/api.py b/altair/vegalite/v4/api.py
--- a/altair/vegalite/v4/api.py
+++ b/altair/vegalite/v4/api.py
@@ -433,14 +433,15 @@ def save(
     ):
         """Save a chart to file in a variety of formats
 
-        Supported formats are json, html, png, svg
+        Supported formats are json, html, png, svg, pdf; the last three require
+        the altair_saver package to be installed.
 
         Parameters
         ----------
         fp : string filename or file-like object
             file in which to write the chart.
         format : string (optional)
-            the format to write: one of ['json', 'html', 'png', 'svg'].
+            the format to write: one of ['json', 'html', 'png', 'svg', 'pdf'].
             If not specified, the format will be determined from the filename.
         override_data_transformer : boolean (optional)
             If True (default), then the save action will be done with


Test patch:
diff --git a/altair/vegalite/v3/tests/test_api.py b/altair/vegalite/v3/tests/test_api.py
--- a/altair/vegalite/v3/tests/test_api.py
+++ b/altair/vegalite/v3/tests/test_api.py
@@ -242,20 +242,28 @@ def test_selection_expression():
     assert selection["value"].to_dict() == "{0}['value']".format(selection.name)
 
 
-@pytest.mark.parametrize("format", ["html", "json", "png", "svg"])
+@pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf"])
 def test_save(format, basic_chart):
-    if format == "png":
+    if format in ["pdf", "png"]:
         out = io.BytesIO()
         mode = "rb"
     else:
         out = io.StringIO()
         mode = "r"
 
-    if format in ["svg", "png"] and not altair_saver:
-        with pytest.raises(ValueError) as err:
-            basic_chart.save(out, format=format)
-        assert "github.com/altair-viz/altair_saver" in str(err.value)
-        return
+    if format in ["svg", "png", "pdf"]:
+        if not altair_saver:
+            with pytest.raises(ValueError) as err:
+                basic_chart.save(out, format=format)
+            assert "github.com/altair-viz/altair_saver" in str(err.value)
+            return
+        elif format not in altair_saver.available_formats():
+            with pytest.raises(ValueError) as err:
+                basic_chart.save(out, format=format)
+            assert f"No enabled saver found that supports format='{format}'" in str(
+                err.value
+            )
+            return
 
     basic_chart.save(out, format=format)
     out.seek(0)
@@ -272,7 +280,7 @@ def test_save(format, basic_chart):
     try:
         basic_chart.save(filename)
         with open(filename, mode) as f:
-            assert f.read() == content
+            assert f.read()[:1000] == content[:1000]
     finally:
         os.remove(filename)
 
diff --git a/altair/vegalite/v4/tests/test_api.py b/altair/vegalite/v4/tests/test_api.py
--- a/altair/vegalite/v4/tests/test_api.py
+++ b/altair/vegalite/v4/tests/test_api.py
@@ -241,20 +241,28 @@ def test_selection_expression():
     assert selection["value"].to_dict() == "{0}['value']".format(selection.name)
 
 
-@pytest.mark.parametrize("format", ["html", "json", "png", "svg"])
+@pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf"])
 def test_save(format, basic_chart):
-    if format == "png":
+    if format in ["pdf", "png"]:
         out = io.BytesIO()
         mode = "rb"
     else:
         out = io.StringIO()
         mode = "r"
 
-    if format in ["svg", "png"] and not altair_saver:
-        with pytest.raises(ValueError) as err:
-            basic_chart.save(out, format=format)
-        assert "github.com/altair-viz/altair_saver" in str(err.value)
-        return
+    if format in ["svg", "png", "pdf"]:
+        if not altair_saver:
+            with pytest.raises(ValueError) as err:
+                basic_chart.save(out, format=format)
+            assert "github.com/altair-viz/altair_saver" in str(err.value)
+            return
+        elif format not in altair_saver.available_formats():
+            with pytest.raises(ValueError) as err:
+                basic_chart.save(out, format=format)
+            assert f"No enabled saver found that supports format='{format}'" in str(
+                err.value
+            )
+            return
 
     basic_chart.save(out, format=format)
     out.seek(0)
@@ -271,7 +279,7 @@ def test_save(format, basic_chart):
     try:
         basic_chart.save(filename)
         with open(filename, mode) as f:
-            assert f.read() == content
+            assert f.read()[:1000] == content[:1000]
     finally:
         os.remove(filename)