streamlit-nightly 1.39.1.dev20241031__py2.py3-none-any.whl → 1.39.1.dev20241101__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- streamlit/commands/logo.py +3 -3
- streamlit/commands/navigation.py +48 -3
- streamlit/commands/page_config.py +3 -3
- streamlit/elements/image.py +26 -441
- streamlit/elements/layouts.py +12 -7
- streamlit/elements/lib/image_utils.py +433 -0
- streamlit/elements/markdown.py +6 -0
- streamlit/elements/metric.py +8 -5
- streamlit/elements/progress.py +2 -1
- streamlit/elements/pyplot.py +3 -5
- streamlit/elements/text.py +1 -1
- streamlit/elements/widgets/audio_input.py +12 -11
- streamlit/elements/widgets/button.py +20 -16
- streamlit/elements/widgets/button_group.py +146 -121
- streamlit/elements/widgets/camera_input.py +13 -11
- streamlit/elements/widgets/chat.py +2 -2
- streamlit/elements/widgets/checkbox.py +30 -24
- streamlit/elements/widgets/color_picker.py +15 -13
- streamlit/elements/widgets/file_uploader.py +12 -12
- streamlit/elements/widgets/multiselect.py +33 -31
- streamlit/elements/widgets/number_input.py +15 -12
- streamlit/elements/widgets/radio.py +15 -12
- streamlit/elements/widgets/select_slider.py +15 -12
- streamlit/elements/widgets/selectbox.py +19 -14
- streamlit/elements/widgets/slider.py +15 -12
- streamlit/elements/widgets/text_widgets.py +33 -27
- streamlit/elements/widgets/time_widgets.py +33 -25
- streamlit/hello/{Animation_Demo.py → animation_demo.py} +9 -10
- streamlit/hello/{Dataframe_Demo.py → dataframe_demo.py} +9 -15
- streamlit/hello/{Hello.py → hello.py} +7 -12
- streamlit/hello/{Mapping_Demo.py → mapping_demo.py} +10 -13
- streamlit/hello/{Plotting_Demo.py → plotting_demo.py} +9 -10
- streamlit/hello/streamlit_app.py +24 -6
- streamlit/proto/Image_pb2.pyi +1 -1
- {streamlit_nightly-1.39.1.dev20241031.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.39.1.dev20241031.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/RECORD +40 -39
- {streamlit_nightly-1.39.1.dev20241031.data → streamlit_nightly-1.39.1.dev20241101.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.39.1.dev20241031.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.39.1.dev20241031.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.39.1.dev20241031.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/top_level.txt +0 -0
streamlit/commands/logo.py
CHANGED
@@ -19,7 +19,7 @@ from __future__ import annotations
|
|
19
19
|
from typing import Literal
|
20
20
|
|
21
21
|
from streamlit import url_util
|
22
|
-
from streamlit.elements.
|
22
|
+
from streamlit.elements.lib.image_utils import AtomicImage, WidthBehavior, image_to_url
|
23
23
|
from streamlit.errors import StreamlitAPIException
|
24
24
|
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
25
25
|
from streamlit.runtime.metrics_util import gather_metrics
|
@@ -132,7 +132,7 @@ def logo(
|
|
132
132
|
try:
|
133
133
|
image_url = image_to_url(
|
134
134
|
image,
|
135
|
-
width=
|
135
|
+
width=WidthBehavior.AUTO,
|
136
136
|
clamp=False,
|
137
137
|
channels="RGB",
|
138
138
|
output_format="auto",
|
@@ -155,7 +155,7 @@ def logo(
|
|
155
155
|
try:
|
156
156
|
icon_image_url = image_to_url(
|
157
157
|
icon_image,
|
158
|
-
width=
|
158
|
+
width=WidthBehavior.AUTO,
|
159
159
|
clamp=False,
|
160
160
|
channels="RGB",
|
161
161
|
output_format="auto",
|
streamlit/commands/navigation.py
CHANGED
@@ -126,15 +126,47 @@ def navigation(
|
|
126
126
|
you pass to ``streamlit run``. Your entrypoint file manages your app's
|
127
127
|
navigation and serves as a router between pages.
|
128
128
|
|
129
|
+
**Example 1: Use a callable or Python file as a page**
|
130
|
+
|
129
131
|
You can declare pages from callables or file paths.
|
130
132
|
|
133
|
+
``page_1.py`` (in the same directory as your entrypoint file):
|
134
|
+
|
135
|
+
>>> import streamlit as st
|
136
|
+
>>>
|
137
|
+
>>> st.title("Page 1")
|
138
|
+
|
139
|
+
Your entrypoint file:
|
140
|
+
|
131
141
|
>>> import streamlit as st
|
132
|
-
>>> from page_functions import page1
|
133
142
|
>>>
|
134
|
-
>>>
|
143
|
+
>>> def page_2():
|
144
|
+
... st.title("Page 2")
|
145
|
+
>>>
|
146
|
+
>>> pg = st.navigation([st.Page("page_1.py"), st.Page(page_2)])
|
135
147
|
>>> pg.run()
|
136
148
|
|
137
|
-
|
149
|
+
.. output::
|
150
|
+
https://doc-navigation-example-1.streamlit.app/
|
151
|
+
height: 200px
|
152
|
+
|
153
|
+
**Example 2: Group pages into sections**
|
154
|
+
|
155
|
+
You can use a dictionary to create sections within your navigation menu. In
|
156
|
+
the following example, each page is similar to Page 1 in Example 1, and all
|
157
|
+
pages are in the same directory. However, you can use Python files from
|
158
|
+
anywhere in your repository. For more information, see |st.Page|_.
|
159
|
+
|
160
|
+
Directory structure:
|
161
|
+
|
162
|
+
>>> your_repository/
|
163
|
+
>>> ├── create_account.py
|
164
|
+
>>> ├── learn.py
|
165
|
+
>>> ├── manage_account.py
|
166
|
+
>>> ├── streamlit_app.py
|
167
|
+
>>> └── trial.py
|
168
|
+
|
169
|
+
``streamlit_app.py``:
|
138
170
|
|
139
171
|
>>> import streamlit as st
|
140
172
|
>>>
|
@@ -152,6 +184,12 @@ def navigation(
|
|
152
184
|
>>> pg = st.navigation(pages)
|
153
185
|
>>> pg.run()
|
154
186
|
|
187
|
+
.. output::
|
188
|
+
https://doc-navigation-example-2.streamlit.app/
|
189
|
+
height: 300px
|
190
|
+
|
191
|
+
**Example 3: Stateful widgets across multiple pages**
|
192
|
+
|
155
193
|
Call widget functions in your entrypoint file when you want a widget to be
|
156
194
|
stateful across pages. Assign keys to your common widgets and access their
|
157
195
|
values through Session State within your pages.
|
@@ -171,6 +209,13 @@ def navigation(
|
|
171
209
|
>>> pg = st.navigation([st.Page(page1), st.Page(page2)])
|
172
210
|
>>> pg.run()
|
173
211
|
|
212
|
+
.. output::
|
213
|
+
https://doc-navigation-multipage-widgets.streamlit.app/
|
214
|
+
height: 350px
|
215
|
+
|
216
|
+
.. |st.Page| replace:: ``st.Page``
|
217
|
+
.. _st.Page: https://docs.streamlit.io/develop/api-reference/navigation/st.page
|
218
|
+
|
174
219
|
"""
|
175
220
|
nav_sections = {"": pages} if isinstance(pages, list) else pages
|
176
221
|
page_list = pages_from_nav_sections(nav_sections)
|
@@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Any, Final, Literal, Mapping, Union, cast
|
|
20
20
|
|
21
21
|
from typing_extensions import TypeAlias
|
22
22
|
|
23
|
-
from streamlit.elements import
|
23
|
+
from streamlit.elements.lib.image_utils import AtomicImage, image_to_url
|
24
24
|
from streamlit.errors import (
|
25
25
|
StreamlitInvalidMenuItemKeyError,
|
26
26
|
StreamlitInvalidPageLayoutError,
|
@@ -41,7 +41,7 @@ GET_HELP_KEY: Final = "get help"
|
|
41
41
|
REPORT_A_BUG_KEY: Final = "report a bug"
|
42
42
|
ABOUT_KEY: Final = "about"
|
43
43
|
|
44
|
-
PageIcon: TypeAlias = Union[
|
44
|
+
PageIcon: TypeAlias = Union[AtomicImage, str]
|
45
45
|
Layout: TypeAlias = Literal["centered", "wide"]
|
46
46
|
InitialSideBarState: TypeAlias = Literal["auto", "expanded", "collapsed"]
|
47
47
|
_GetHelp: TypeAlias = Literal["Get help", "Get Help", "get help"]
|
@@ -107,7 +107,7 @@ def _get_favicon_string(page_icon: PageIcon) -> str:
|
|
107
107
|
|
108
108
|
# Fall back to image_to_url.
|
109
109
|
try:
|
110
|
-
return
|
110
|
+
return image_to_url(
|
111
111
|
page_icon,
|
112
112
|
width=-1, # Always use full width for favicons
|
113
113
|
clamp=False,
|
streamlit/elements/image.py
CHANGED
@@ -21,69 +21,26 @@
|
|
21
21
|
|
22
22
|
from __future__ import annotations
|
23
23
|
|
24
|
-
import
|
25
|
-
import os
|
26
|
-
import re
|
27
|
-
from enum import IntEnum
|
28
|
-
from typing import TYPE_CHECKING, Final, Literal, Sequence, Union, cast
|
24
|
+
from typing import TYPE_CHECKING, Literal, Union, cast
|
29
25
|
|
30
26
|
from typing_extensions import TypeAlias
|
31
27
|
|
32
|
-
from streamlit import runtime, url_util
|
33
28
|
from streamlit.deprecation_util import show_deprecation_warning
|
29
|
+
from streamlit.elements.lib.image_utils import (
|
30
|
+
Channels,
|
31
|
+
ImageFormatOrAuto,
|
32
|
+
ImageOrImageList,
|
33
|
+
WidthBehavior,
|
34
|
+
marshall_images,
|
35
|
+
)
|
34
36
|
from streamlit.errors import StreamlitAPIException
|
35
37
|
from streamlit.proto.Image_pb2 import ImageList as ImageListProto
|
36
|
-
from streamlit.runtime import caching
|
37
38
|
from streamlit.runtime.metrics_util import gather_metrics
|
38
|
-
from streamlit.type_util import NumpyShape
|
39
39
|
|
40
40
|
if TYPE_CHECKING:
|
41
|
-
from typing import Any
|
42
|
-
|
43
|
-
import numpy.typing as npt
|
44
|
-
from PIL import GifImagePlugin, Image, ImageFile
|
45
|
-
|
46
41
|
from streamlit.delta_generator import DeltaGenerator
|
47
42
|
|
48
|
-
# This constant is related to the frontend maximum content width specified
|
49
|
-
# in App.jsx main container
|
50
|
-
# 730 is the max width of element-container in the frontend, and 2x is for high
|
51
|
-
# DPI.
|
52
|
-
MAXIMUM_CONTENT_WIDTH: Final[int] = 2 * 730
|
53
|
-
|
54
|
-
PILImage: TypeAlias = Union[
|
55
|
-
"ImageFile.ImageFile", "Image.Image", "GifImagePlugin.GifImageFile"
|
56
|
-
]
|
57
|
-
AtomicImage: TypeAlias = Union[PILImage, "npt.NDArray[Any]", io.BytesIO, str, bytes]
|
58
|
-
ImageOrImageList: TypeAlias = Union[AtomicImage, Sequence[AtomicImage]]
|
59
43
|
UseColumnWith: TypeAlias = Union[Literal["auto", "always", "never"], bool, None]
|
60
|
-
Channels: TypeAlias = Literal["RGB", "BGR"]
|
61
|
-
ImageFormat: TypeAlias = Literal["JPEG", "PNG", "GIF"]
|
62
|
-
ImageFormatOrAuto: TypeAlias = Literal[ImageFormat, "auto"]
|
63
|
-
|
64
|
-
|
65
|
-
# @see Image.proto
|
66
|
-
# @see WidthBehavior on the frontend
|
67
|
-
class WidthBehaviour(IntEnum):
|
68
|
-
"""
|
69
|
-
Special values that are recognized by the frontend and allow us to change the
|
70
|
-
behavior of the displayed image.
|
71
|
-
"""
|
72
|
-
|
73
|
-
ORIGINAL = -1
|
74
|
-
COLUMN = -2
|
75
|
-
AUTO = -3
|
76
|
-
MIN_IMAGE_OR_CONTAINER = -4
|
77
|
-
MAX_IMAGE_OR_CONTAINER = -5
|
78
|
-
|
79
|
-
|
80
|
-
WidthBehaviour.ORIGINAL.__doc__ = """Display the image at its original width"""
|
81
|
-
WidthBehaviour.COLUMN.__doc__ = (
|
82
|
-
"""Display the image at the width of the column it's in."""
|
83
|
-
)
|
84
|
-
WidthBehaviour.AUTO.__doc__ = """Display the image at its original width, unless it
|
85
|
-
would exceed the width of its column in which case clamp it to
|
86
|
-
its column width"""
|
87
44
|
|
88
45
|
|
89
46
|
class ImageMixin:
|
@@ -118,18 +75,16 @@ class ImageMixin:
|
|
118
75
|
Image caption. If displaying multiple images, caption should be a
|
119
76
|
list of captions (one for each image).
|
120
77
|
width : int or None
|
121
|
-
Image width. None
|
122
|
-
|
123
|
-
|
78
|
+
Image width. If this is ``None`` (default), Streamlit will use the
|
79
|
+
image's native width, up to the width of the parent container.
|
80
|
+
When using an SVG image without a default width, you should declare
|
81
|
+
``width`` or use ``use_container_width=True``.
|
124
82
|
use_column_width : "auto", "always", "never", or bool
|
125
83
|
If "auto", set the image's width to its natural size,
|
126
84
|
but do not exceed the width of the column.
|
127
85
|
If "always" or True, set the image's width to the column width.
|
128
86
|
If "never" or False, set the image's width to its natural size.
|
129
87
|
Note: if set, `use_column_width` takes precedence over the `width` parameter.
|
130
|
-
.. deprecated::
|
131
|
-
The `use_column_width` parameter has been deprecated and will be removed in a future release.
|
132
|
-
Please utilize the `use_container_width` parameter instead.
|
133
88
|
clamp : bool
|
134
89
|
Clamp image pixel values to a valid range ([0-255] per channel).
|
135
90
|
This is only meaningful for byte array images; the parameter is
|
@@ -148,14 +103,15 @@ class ImageMixin:
|
|
148
103
|
Defaults to "auto" which identifies the compression type based
|
149
104
|
on the type and format of the image argument.
|
150
105
|
use_container_width : bool
|
151
|
-
Whether to override
|
152
|
-
parent container. If ``use_container_width`` is ``True``, Streamlit
|
153
|
-
sets the width of the figure to match the width of the parent
|
106
|
+
Whether to override ``width`` with the width of the parent
|
154
107
|
container. If ``use_container_width`` is ``False`` (default),
|
155
|
-
Streamlit sets the width
|
156
|
-
|
157
|
-
|
158
|
-
|
108
|
+
Streamlit sets the image's width according to ``width``. If
|
109
|
+
``use_container_width`` is ``True``, Streamlit sets the width of
|
110
|
+
the image to match the width of the parent container.
|
111
|
+
|
112
|
+
.. deprecated::
|
113
|
+
``use_column_width`` is deprecated and will be removed in a future
|
114
|
+
release. Please use the ``use_container_width`` parameter instead.
|
159
115
|
|
160
116
|
Example
|
161
117
|
-------
|
@@ -175,7 +131,7 @@ class ImageMixin:
|
|
175
131
|
)
|
176
132
|
|
177
133
|
image_width: int = (
|
178
|
-
|
134
|
+
WidthBehavior.ORIGINAL if (width is None or width <= 0) else width
|
179
135
|
)
|
180
136
|
|
181
137
|
if use_column_width is not None:
|
@@ -185,21 +141,21 @@ class ImageMixin:
|
|
185
141
|
)
|
186
142
|
|
187
143
|
if use_column_width == "auto":
|
188
|
-
image_width =
|
144
|
+
image_width = WidthBehavior.AUTO
|
189
145
|
elif use_column_width == "always" or use_column_width is True:
|
190
|
-
image_width =
|
146
|
+
image_width = WidthBehavior.COLUMN
|
191
147
|
elif use_column_width == "never" or use_column_width is False:
|
192
|
-
image_width =
|
148
|
+
image_width = WidthBehavior.ORIGINAL
|
193
149
|
|
194
150
|
else:
|
195
151
|
if use_container_width is True:
|
196
|
-
image_width =
|
152
|
+
image_width = WidthBehavior.MAX_IMAGE_OR_CONTAINER
|
197
153
|
elif image_width is not None and image_width > 0:
|
198
154
|
# Use the given width. It will be capped on the frontend if it
|
199
155
|
# exceeds the container width.
|
200
156
|
pass
|
201
157
|
elif use_container_width is False:
|
202
|
-
image_width =
|
158
|
+
image_width = WidthBehavior.MIN_IMAGE_OR_CONTAINER
|
203
159
|
|
204
160
|
image_list_proto = ImageListProto()
|
205
161
|
marshall_images(
|
@@ -218,374 +174,3 @@ class ImageMixin:
|
|
218
174
|
def dg(self) -> DeltaGenerator:
|
219
175
|
"""Get our DeltaGenerator."""
|
220
176
|
return cast("DeltaGenerator", self)
|
221
|
-
|
222
|
-
|
223
|
-
def _image_may_have_alpha_channel(image: PILImage) -> bool:
|
224
|
-
return image.mode in ("RGBA", "LA", "P")
|
225
|
-
|
226
|
-
|
227
|
-
def _image_is_gif(image: PILImage) -> bool:
|
228
|
-
return image.format == "GIF"
|
229
|
-
|
230
|
-
|
231
|
-
def _validate_image_format_string(
|
232
|
-
image_data: bytes | PILImage, format: str
|
233
|
-
) -> ImageFormat:
|
234
|
-
"""Return either "JPEG", "PNG", or "GIF", based on the input `format` string.
|
235
|
-
|
236
|
-
- If `format` is "JPEG" or "JPG" (or any capitalization thereof), return "JPEG"
|
237
|
-
- If `format` is "PNG" (or any capitalization thereof), return "PNG"
|
238
|
-
- For all other strings, return "PNG" if the image has an alpha channel,
|
239
|
-
"GIF" if the image is a GIF, and "JPEG" otherwise.
|
240
|
-
"""
|
241
|
-
format = format.upper()
|
242
|
-
if format in {"JPEG", "PNG"}:
|
243
|
-
return cast(ImageFormat, format)
|
244
|
-
|
245
|
-
# We are forgiving on the spelling of JPEG
|
246
|
-
if format == "JPG":
|
247
|
-
return "JPEG"
|
248
|
-
|
249
|
-
pil_image: PILImage
|
250
|
-
if isinstance(image_data, bytes):
|
251
|
-
from PIL import Image
|
252
|
-
|
253
|
-
pil_image = Image.open(io.BytesIO(image_data))
|
254
|
-
else:
|
255
|
-
pil_image = image_data
|
256
|
-
|
257
|
-
if _image_is_gif(pil_image):
|
258
|
-
return "GIF"
|
259
|
-
|
260
|
-
if _image_may_have_alpha_channel(pil_image):
|
261
|
-
return "PNG"
|
262
|
-
|
263
|
-
return "JPEG"
|
264
|
-
|
265
|
-
|
266
|
-
def _PIL_to_bytes(
|
267
|
-
image: PILImage,
|
268
|
-
format: ImageFormat = "JPEG",
|
269
|
-
quality: int = 100,
|
270
|
-
) -> bytes:
|
271
|
-
"""Convert a PIL image to bytes."""
|
272
|
-
tmp = io.BytesIO()
|
273
|
-
|
274
|
-
# User must have specified JPEG, so we must convert it
|
275
|
-
if format == "JPEG" and _image_may_have_alpha_channel(image):
|
276
|
-
image = image.convert("RGB")
|
277
|
-
|
278
|
-
image.save(tmp, format=format, quality=quality)
|
279
|
-
|
280
|
-
return tmp.getvalue()
|
281
|
-
|
282
|
-
|
283
|
-
def _BytesIO_to_bytes(data: io.BytesIO) -> bytes:
|
284
|
-
data.seek(0)
|
285
|
-
return data.getvalue()
|
286
|
-
|
287
|
-
|
288
|
-
def _np_array_to_bytes(array: npt.NDArray[Any], output_format: str = "JPEG") -> bytes:
|
289
|
-
import numpy as np
|
290
|
-
from PIL import Image
|
291
|
-
|
292
|
-
img = Image.fromarray(array.astype(np.uint8))
|
293
|
-
format = _validate_image_format_string(img, output_format)
|
294
|
-
|
295
|
-
return _PIL_to_bytes(img, format)
|
296
|
-
|
297
|
-
|
298
|
-
def _4d_to_list_3d(array: npt.NDArray[Any]) -> list[npt.NDArray[Any]]:
|
299
|
-
return [array[i, :, :, :] for i in range(0, array.shape[0])]
|
300
|
-
|
301
|
-
|
302
|
-
def _verify_np_shape(array: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
303
|
-
shape: NumpyShape = array.shape
|
304
|
-
if len(shape) not in (2, 3):
|
305
|
-
raise StreamlitAPIException("Numpy shape has to be of length 2 or 3.")
|
306
|
-
if len(shape) == 3 and shape[-1] not in (1, 3, 4):
|
307
|
-
raise StreamlitAPIException(
|
308
|
-
"Channel can only be 1, 3, or 4 got %d. Shape is %s"
|
309
|
-
% (shape[-1], str(shape))
|
310
|
-
)
|
311
|
-
|
312
|
-
# If there's only one channel, convert is to x, y
|
313
|
-
if len(shape) == 3 and shape[-1] == 1:
|
314
|
-
array = array[:, :, 0]
|
315
|
-
|
316
|
-
return array
|
317
|
-
|
318
|
-
|
319
|
-
def _get_image_format_mimetype(image_format: ImageFormat) -> str:
|
320
|
-
"""Get the mimetype string for the given ImageFormat."""
|
321
|
-
return f"image/{image_format.lower()}"
|
322
|
-
|
323
|
-
|
324
|
-
def _ensure_image_size_and_format(
|
325
|
-
image_data: bytes, width: int, image_format: ImageFormat
|
326
|
-
) -> bytes:
|
327
|
-
"""Resize an image if it exceeds the given width, or if exceeds
|
328
|
-
MAXIMUM_CONTENT_WIDTH. Ensure the image's format corresponds to the given
|
329
|
-
ImageFormat. Return the (possibly resized and reformatted) image bytes.
|
330
|
-
"""
|
331
|
-
from PIL import Image
|
332
|
-
|
333
|
-
pil_image: PILImage = Image.open(io.BytesIO(image_data))
|
334
|
-
actual_width, actual_height = pil_image.size
|
335
|
-
|
336
|
-
if width < 0 and actual_width > MAXIMUM_CONTENT_WIDTH:
|
337
|
-
width = MAXIMUM_CONTENT_WIDTH
|
338
|
-
|
339
|
-
if width > 0 and actual_width > width:
|
340
|
-
# We need to resize the image.
|
341
|
-
new_height = int(1.0 * actual_height * width / actual_width)
|
342
|
-
# pillow reexports Image.Resampling.BILINEAR as Image.BILINEAR for backwards
|
343
|
-
# compatibility reasons, so we use the reexport to support older pillow
|
344
|
-
# versions. The types don't seem to reflect this, though, hence the type: ignore
|
345
|
-
# below.
|
346
|
-
pil_image = pil_image.resize((width, new_height), resample=Image.BILINEAR) # type: ignore[attr-defined]
|
347
|
-
return _PIL_to_bytes(pil_image, format=image_format, quality=90)
|
348
|
-
|
349
|
-
if pil_image.format != image_format:
|
350
|
-
# We need to reformat the image.
|
351
|
-
return _PIL_to_bytes(pil_image, format=image_format, quality=90)
|
352
|
-
|
353
|
-
# No resizing or reformatting necessary - return the original bytes.
|
354
|
-
return image_data
|
355
|
-
|
356
|
-
|
357
|
-
def _clip_image(image: npt.NDArray[Any], clamp: bool) -> npt.NDArray[Any]:
|
358
|
-
import numpy as np
|
359
|
-
|
360
|
-
data = image
|
361
|
-
if issubclass(image.dtype.type, np.floating):
|
362
|
-
if clamp:
|
363
|
-
data = np.clip(image, 0, 1.0)
|
364
|
-
else:
|
365
|
-
if np.amin(image) < 0.0 or np.amax(image) > 1.0:
|
366
|
-
raise RuntimeError("Data is outside [0.0, 1.0] and clamp is not set.")
|
367
|
-
data = data * 255
|
368
|
-
else:
|
369
|
-
if clamp:
|
370
|
-
data = np.clip(image, 0, 255)
|
371
|
-
else:
|
372
|
-
if np.amin(image) < 0 or np.amax(image) > 255:
|
373
|
-
raise RuntimeError("Data is outside [0, 255] and clamp is not set.")
|
374
|
-
return data
|
375
|
-
|
376
|
-
|
377
|
-
def image_to_url(
|
378
|
-
image: AtomicImage,
|
379
|
-
width: int,
|
380
|
-
clamp: bool,
|
381
|
-
channels: Channels,
|
382
|
-
output_format: ImageFormatOrAuto,
|
383
|
-
image_id: str,
|
384
|
-
) -> str:
|
385
|
-
"""Return a URL that an image can be served from.
|
386
|
-
If `image` is already a URL, return it unmodified.
|
387
|
-
Otherwise, add the image to the MediaFileManager and return the URL.
|
388
|
-
|
389
|
-
(When running in "raw" mode, we won't actually load data into the
|
390
|
-
MediaFileManager, and we'll return an empty URL.)
|
391
|
-
"""
|
392
|
-
import numpy as np
|
393
|
-
from PIL import Image, ImageFile
|
394
|
-
|
395
|
-
image_data: bytes
|
396
|
-
|
397
|
-
# Strings
|
398
|
-
if isinstance(image, str):
|
399
|
-
if not os.path.isfile(image) and url_util.is_url(
|
400
|
-
image, allowed_schemas=("http", "https", "data")
|
401
|
-
):
|
402
|
-
# If it's a url, return it directly.
|
403
|
-
return image
|
404
|
-
|
405
|
-
if image.endswith(".svg") and os.path.isfile(image):
|
406
|
-
# Unpack local SVG image file to an SVG string
|
407
|
-
with open(image) as textfile:
|
408
|
-
image = textfile.read()
|
409
|
-
|
410
|
-
# Following regex allows svg image files to start either via a "<?xml...>" tag
|
411
|
-
# eventually followed by a "<svg...>" tag or directly starting with a "<svg>" tag
|
412
|
-
if re.search(r"(^\s?(<\?xml[\s\S]*<svg\s)|^\s?<svg\s|^\s?<svg>\s)", image):
|
413
|
-
if "xmlns" not in image:
|
414
|
-
# The xmlns attribute is required for SVGs to render in an img tag.
|
415
|
-
# If it's not present, we add to the first SVG tag:
|
416
|
-
image = image.replace(
|
417
|
-
"<svg", '<svg xmlns="http://www.w3.org/2000/svg" ', 1
|
418
|
-
)
|
419
|
-
# Convert to base64 to prevent issues with encoding:
|
420
|
-
import base64
|
421
|
-
|
422
|
-
image_b64_encoded = base64.b64encode(image.encode("utf-8")).decode("utf-8")
|
423
|
-
# Return SVG as data URI:
|
424
|
-
return f"data:image/svg+xml;base64,{image_b64_encoded}"
|
425
|
-
|
426
|
-
# Otherwise, try to open it as a file.
|
427
|
-
try:
|
428
|
-
with open(image, "rb") as f:
|
429
|
-
image_data = f.read()
|
430
|
-
except Exception:
|
431
|
-
# When we aren't able to open the image file, we still pass the path to
|
432
|
-
# the MediaFileManager - its storage backend may have access to files
|
433
|
-
# that Streamlit does not.
|
434
|
-
import mimetypes
|
435
|
-
|
436
|
-
mimetype, _ = mimetypes.guess_type(image)
|
437
|
-
if mimetype is None:
|
438
|
-
mimetype = "application/octet-stream"
|
439
|
-
|
440
|
-
url = runtime.get_instance().media_file_mgr.add(image, mimetype, image_id)
|
441
|
-
caching.save_media_data(image, mimetype, image_id)
|
442
|
-
return url
|
443
|
-
|
444
|
-
# PIL Images
|
445
|
-
elif isinstance(image, (ImageFile.ImageFile, Image.Image)):
|
446
|
-
format = _validate_image_format_string(image, output_format)
|
447
|
-
image_data = _PIL_to_bytes(image, format)
|
448
|
-
|
449
|
-
# BytesIO
|
450
|
-
# Note: This doesn't support SVG. We could convert to png (cairosvg.svg2png)
|
451
|
-
# or just decode BytesIO to string and handle that way.
|
452
|
-
elif isinstance(image, io.BytesIO):
|
453
|
-
image_data = _BytesIO_to_bytes(image)
|
454
|
-
|
455
|
-
# Numpy Arrays (ie opencv)
|
456
|
-
elif isinstance(image, np.ndarray):
|
457
|
-
image = _clip_image(
|
458
|
-
_verify_np_shape(image),
|
459
|
-
clamp,
|
460
|
-
)
|
461
|
-
|
462
|
-
if channels == "BGR":
|
463
|
-
if len(cast(NumpyShape, image.shape)) == 3:
|
464
|
-
image = image[:, :, [2, 1, 0]]
|
465
|
-
else:
|
466
|
-
raise StreamlitAPIException(
|
467
|
-
'When using `channels="BGR"`, the input image should '
|
468
|
-
"have exactly 3 color channels"
|
469
|
-
)
|
470
|
-
|
471
|
-
# Depending on the version of numpy that the user has installed, the
|
472
|
-
# typechecker may not be able to deduce that indexing into a
|
473
|
-
# `npt.NDArray[Any]` returns a `npt.NDArray[Any]`, so we need to
|
474
|
-
# ignore redundant casts below.
|
475
|
-
image_data = _np_array_to_bytes(
|
476
|
-
array=cast("npt.NDArray[Any]", image), # type: ignore[redundant-cast]
|
477
|
-
output_format=output_format,
|
478
|
-
)
|
479
|
-
|
480
|
-
# Raw bytes
|
481
|
-
else:
|
482
|
-
image_data = image
|
483
|
-
|
484
|
-
# Determine the image's format, resize it, and get its mimetype
|
485
|
-
image_format = _validate_image_format_string(image_data, output_format)
|
486
|
-
image_data = _ensure_image_size_and_format(image_data, width, image_format)
|
487
|
-
mimetype = _get_image_format_mimetype(image_format)
|
488
|
-
|
489
|
-
if runtime.exists():
|
490
|
-
url = runtime.get_instance().media_file_mgr.add(image_data, mimetype, image_id)
|
491
|
-
caching.save_media_data(image_data, mimetype, image_id)
|
492
|
-
return url
|
493
|
-
else:
|
494
|
-
# When running in "raw mode", we can't access the MediaFileManager.
|
495
|
-
return ""
|
496
|
-
|
497
|
-
|
498
|
-
def marshall_images(
|
499
|
-
coordinates: str,
|
500
|
-
image: ImageOrImageList,
|
501
|
-
caption: str | npt.NDArray[Any] | list[str] | None,
|
502
|
-
width: int | WidthBehaviour,
|
503
|
-
proto_imgs: ImageListProto,
|
504
|
-
clamp: bool,
|
505
|
-
channels: Channels = "RGB",
|
506
|
-
output_format: ImageFormatOrAuto = "auto",
|
507
|
-
) -> None:
|
508
|
-
"""Fill an ImageListProto with a list of images and their captions.
|
509
|
-
|
510
|
-
The images will be resized and reformatted as necessary.
|
511
|
-
|
512
|
-
Parameters
|
513
|
-
----------
|
514
|
-
coordinates
|
515
|
-
A string indentifying the images' location in the frontend.
|
516
|
-
image
|
517
|
-
The image or images to include in the ImageListProto.
|
518
|
-
caption
|
519
|
-
Image caption. If displaying multiple images, caption should be a
|
520
|
-
list of captions (one for each image).
|
521
|
-
width
|
522
|
-
The desired width of the image or images. This parameter will be
|
523
|
-
passed to the frontend.
|
524
|
-
Positive values set the image width explicitly.
|
525
|
-
Negative values has some special. For details, see: `WidthBehaviour`
|
526
|
-
proto_imgs
|
527
|
-
The ImageListProto to fill in.
|
528
|
-
clamp
|
529
|
-
Clamp image pixel values to a valid range ([0-255] per channel).
|
530
|
-
This is only meaningful for byte array images; the parameter is
|
531
|
-
ignored for image URLs. If this is not set, and an image has an
|
532
|
-
out-of-range value, an error will be thrown.
|
533
|
-
channels
|
534
|
-
If image is an nd.array, this parameter denotes the format used to
|
535
|
-
represent color information. Defaults to 'RGB', meaning
|
536
|
-
`image[:, :, 0]` is the red channel, `image[:, :, 1]` is green, and
|
537
|
-
`image[:, :, 2]` is blue. For images coming from libraries like
|
538
|
-
OpenCV you should set this to 'BGR', instead.
|
539
|
-
output_format
|
540
|
-
This parameter specifies the format to use when transferring the
|
541
|
-
image data. Photos should use the JPEG format for lossy compression
|
542
|
-
while diagrams should use the PNG format for lossless compression.
|
543
|
-
Defaults to 'auto' which identifies the compression type based
|
544
|
-
on the type and format of the image argument.
|
545
|
-
"""
|
546
|
-
import numpy as np
|
547
|
-
|
548
|
-
channels = cast(Channels, channels.upper())
|
549
|
-
|
550
|
-
# Turn single image and caption into one element list.
|
551
|
-
images: Sequence[AtomicImage]
|
552
|
-
if isinstance(image, (list, set, tuple)):
|
553
|
-
images = list(image)
|
554
|
-
elif isinstance(image, np.ndarray) and len(cast(NumpyShape, image.shape)) == 4:
|
555
|
-
images = _4d_to_list_3d(image)
|
556
|
-
else:
|
557
|
-
images = [image] # type: ignore
|
558
|
-
|
559
|
-
if isinstance(caption, list):
|
560
|
-
captions: Sequence[str | None] = caption
|
561
|
-
elif isinstance(caption, str):
|
562
|
-
captions = [caption]
|
563
|
-
elif isinstance(caption, np.ndarray) and len(cast(NumpyShape, caption.shape)) == 1:
|
564
|
-
captions = caption.tolist()
|
565
|
-
elif caption is None:
|
566
|
-
captions = [None] * len(images)
|
567
|
-
else:
|
568
|
-
captions = [str(caption)]
|
569
|
-
|
570
|
-
assert isinstance(
|
571
|
-
captions, list
|
572
|
-
), "If image is a list then caption should be as well"
|
573
|
-
assert len(captions) == len(images), "Cannot pair %d captions with %d images." % (
|
574
|
-
len(captions),
|
575
|
-
len(images),
|
576
|
-
)
|
577
|
-
|
578
|
-
proto_imgs.width = int(width)
|
579
|
-
# Each image in an image list needs to be kept track of at its own coordinates.
|
580
|
-
for coord_suffix, (image, caption) in enumerate(zip(images, captions)):
|
581
|
-
proto_img = proto_imgs.imgs.add()
|
582
|
-
if caption is not None:
|
583
|
-
proto_img.caption = str(caption)
|
584
|
-
|
585
|
-
# We use the index of the image in the input image list to identify this image inside
|
586
|
-
# MediaFileManager. For this, we just add the index to the image's "coordinates".
|
587
|
-
image_id = "%s-%i" % (coordinates, coord_suffix)
|
588
|
-
|
589
|
-
proto_img.url = image_to_url(
|
590
|
-
image, width, clamp, channels, output_format, image_id
|
591
|
-
)
|