Repository: streamlit/streamlit

Issue:
SVG support
## Is your feature request related to a problem? Please describe the problem.

I want a way to output svg graphics in Python. [I'm not the only one](https://discuss.streamlit.io/t/display-svg/172)!

## Describe the solution you'd like

I would love for `st.write(...)` to support an existing svg python library like
* [`svgwrite`](https://pypi.org/project/svgwrite/)
* [`svglib`](https://pypi.org/project/svglib/)
* [`CairoSVG`](https://cairosvg.org/)
(Never tried these, so not sure which is good, but we should pick the best / most widely used one.)

## Describe alternatives you've considered
We could also look to see if Jupyter supports SVG and which libraries they're using.



Discussion:
Hi!
This would be great!
My personal goal is to be able to display SVG + XML images on Streamlit.
It is possible to embed such an image with `st.write(html_for_svg, unsafe_allow_html=True)` but it require to write HTML code in the `html_for_svg` variable:
 `<figure><embed type="image/svg+xml" src="accessible/path/to/image.svg" /></figure>`
and to have the svg image somewhere directly accessible, which is not the case by default.
This is a workaround to display SVGs in Streamlit apps now: https://discuss.streamlit.io/t/display-svg/172/5?u=adrien_treuille

[Here is an additional forum conversation](https://discuss.streamlit.io/t/include-svg-image-as-part-of-markdown/1314/4) regarding this enhancement.
A few additional notes from offline conversations:

- It would be great if we could autodetect the ``format`` parameter too.
- These should all be viable:

```
st.image(url_to_svg, format="svg")
st.image(svg_string, format="svg")
st.image(stringio_with_svg, format="svg")  # (So it supports files)
```

I think TC's approach is the cleanest route eng-wise, and it makes a lot of sense in terms of API too. So I'll flesh out the spec a little more here. 

We can talk about whether we want `st.write` to support SVG strings later on, as that proposal needs a little more thinking. 

### Proposal 

The `st.image` function signature today looks like this:

```
    def image(
        self,
        element,
        image,
        caption=None,
        width=None,
        use_column_width=False,
        clamp=False,
        channels="RGB",
        format="JPEG",
    ):
```

Here are the proposed changes:
* `image` should accept SVG file-like objects (i.e. BytesIO) as well as SVG strings. (And also arrays of either of those, since `st.image` lets you specify multiple images).
* If `image` is a string, we need to detect whether it's a URL or SVG.
* SVG data should be transferred "as is" to the frontend, without being rasterized into PNG or JPG. The frontend then just renders pure SVG normally.
* `clamp`, `channels` don't make sense for a vector file format. We should add some text to the docstring explaining that. And we should also consider showing warnings to users when they fill out those fields and the input is SVG.
* `format` doesn't make sense for SVG because it's actually used to convert a raw image array into either JPG or PNG. So should add some text to `format`'s docstring, explaining that it doesn't apply in this case. In fact we should rename that option, as described in https://github.com/streamlit/streamlit/issues/1137 , but that's another story! 
Just to make sure, when @tvst refers to the case when the value of `image` is either URL or SVG, does it also cover the case of a local file of type SVG?
@drorata : I believe that for that case you'd have to first open the file with [Python's `open()` function](https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files), then pass it into `st.image()`.

I hope you and your loved ones are safe and well! ✌️

Resolving patch:
diff --git a/lib/streamlit/delta_generator.py b/lib/streamlit/delta_generator.py
--- a/lib/streamlit/delta_generator.py
+++ b/lib/streamlit/delta_generator.py
@@ -1527,12 +1527,14 @@ def image(
             OR a color image of shape (w,h,3)
             OR an RGBA image of shape (w,h,4)
             OR a URL to fetch the image from
+            OR an SVG XML string like `<svg xmlns=...</svg>`
             OR a list of one of the above, to display multiple images.
         caption : str or list of str
             Image caption. If displaying multiple images, caption should be a
             list of captions (one for each image).
         width : int or None
             Image width. None means use the image width.
+            Should be set for SVG images, as they have no default image width.
         use_column_width : bool
             If True, set the image width to the column width. This takes
             precedence over the `width` parameter.
diff --git a/lib/streamlit/elements/image_proto.py b/lib/streamlit/elements/image_proto.py
--- a/lib/streamlit/elements/image_proto.py
+++ b/lib/streamlit/elements/image_proto.py
@@ -24,6 +24,7 @@
 
 from streamlit.errors import StreamlitAPIException
 from streamlit.logger import get_logger
+from urllib.parse import quote
 from urllib.parse import urlparse
 
 from streamlit.media_file_manager import media_file_manager
@@ -148,6 +149,8 @@ def image_to_url(image, width, clamp, channels, format, image_id, allow_emoji=Fa
         data = _PIL_to_bytes(image, format)
 
     # BytesIO
+    # Note: This doesn't support SVG. We could convert to png (cairosvg.svg2png)
+    # or just decode BytesIO to string and handle that way.
     elif type(image) is io.BytesIO:
         data = _BytesIO_to_bytes(image)
 
@@ -177,7 +180,15 @@ def image_to_url(image, width, clamp, channels, format, image_id, allow_emoji=Fa
         except UnicodeDecodeError:
             pass
 
-        # If not, see if it's a file.
+        # Unpack local SVG image file to an SVG string
+        if image.endswith(".svg"):
+            with open(image) as textfile:
+                image = textfile.read()
+        # If it's an SVG string, then format and return an SVG data url
+        if image.startswith("<svg"):
+            return f"data:image/svg+xml,{image}"
+
+        # Finally, see if it's a file.
         try:
             with open(image, "rb") as f:
                 data = f.read()


Test patch:
diff --git a/frontend/src/lib/UriUtil.test.ts b/frontend/src/lib/UriUtil.test.ts
--- a/frontend/src/lib/UriUtil.test.ts
+++ b/frontend/src/lib/UriUtil.test.ts
@@ -168,3 +168,11 @@ test("passes through other media uris", () => {
 
   expect(uri).toBe("http://example/blah.png")
 })
+
+test("sanitizes SVG uris", () => {
+  const uri = buildMediaUri(
+    `data:image/svg+xml,<svg><script>alert('evil')</script></svg>`
+  )
+
+  expect(uri).toBe(`data:image/svg+xml,%3Csvg%3E%3C%2Fsvg%3E`)
+})
diff --git a/lib/tests/streamlit/image_proto_test.py b/lib/tests/streamlit/image_proto_test.py
--- a/lib/tests/streamlit/image_proto_test.py
+++ b/lib/tests/streamlit/image_proto_test.py
@@ -126,6 +126,29 @@ def test_marshall_images(self, data_in, format):
         self.assertTrue(imglist.imgs[0].url.startswith("/media"))
         self.assertTrue(file_id in imglist.imgs[0].url)
 
+    @parameterized.expand(
+        [
+            (IMAGES["img_32_32_3_rgb"]["np"], "/media/",),
+            ("https://streamlit.io/test.png", "https://streamlit.io/test.png",),
+            ("<svg fake></svg>", "data:image/svg+xml,<svg fake></svg>",),
+            ("🦈", "🦈",),
+            (":shark:", ":shark:",),
+        ]
+    )
+    def test_image_to_url(self, image, expected_prefix):
+        from streamlit.elements.image_proto import image_to_url
+
+        url = image_to_url(
+            image,
+            width=-1,
+            clamp=False,
+            channels="RGB",
+            format="JPEG",
+            image_id="blah",
+            allow_emoji=True,
+        )
+        self.assertTrue(url.startswith(expected_prefix))
+
     def test_BytesIO_to_bytes(self):
         """Test streamlit.image_proto.BytesIO_to_bytes."""
         pass