streamlit-nightly 1.39.1.dev20241030__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.
Files changed (41) hide show
  1. streamlit/commands/logo.py +3 -3
  2. streamlit/commands/navigation.py +48 -3
  3. streamlit/commands/page_config.py +3 -3
  4. streamlit/elements/image.py +26 -441
  5. streamlit/elements/layouts.py +12 -7
  6. streamlit/elements/lib/image_utils.py +433 -0
  7. streamlit/elements/markdown.py +6 -0
  8. streamlit/elements/metric.py +8 -5
  9. streamlit/elements/progress.py +2 -1
  10. streamlit/elements/pyplot.py +3 -5
  11. streamlit/elements/text.py +1 -1
  12. streamlit/elements/widgets/audio_input.py +12 -11
  13. streamlit/elements/widgets/button.py +20 -16
  14. streamlit/elements/widgets/button_group.py +146 -121
  15. streamlit/elements/widgets/camera_input.py +13 -11
  16. streamlit/elements/widgets/chat.py +2 -2
  17. streamlit/elements/widgets/checkbox.py +30 -24
  18. streamlit/elements/widgets/color_picker.py +15 -13
  19. streamlit/elements/widgets/file_uploader.py +12 -12
  20. streamlit/elements/widgets/multiselect.py +33 -31
  21. streamlit/elements/widgets/number_input.py +15 -12
  22. streamlit/elements/widgets/radio.py +15 -12
  23. streamlit/elements/widgets/select_slider.py +15 -12
  24. streamlit/elements/widgets/selectbox.py +19 -14
  25. streamlit/elements/widgets/slider.py +15 -12
  26. streamlit/elements/widgets/text_widgets.py +33 -27
  27. streamlit/elements/widgets/time_widgets.py +33 -25
  28. streamlit/hello/{Animation_Demo.py → animation_demo.py} +9 -10
  29. streamlit/hello/{Dataframe_Demo.py → dataframe_demo.py} +9 -15
  30. streamlit/hello/{Hello.py → hello.py} +7 -12
  31. streamlit/hello/{Mapping_Demo.py → mapping_demo.py} +10 -13
  32. streamlit/hello/{Plotting_Demo.py → plotting_demo.py} +9 -10
  33. streamlit/hello/streamlit_app.py +24 -6
  34. streamlit/proto/Image_pb2.pyi +1 -1
  35. streamlit/web/server/server.py +6 -1
  36. {streamlit_nightly-1.39.1.dev20241030.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/METADATA +1 -1
  37. {streamlit_nightly-1.39.1.dev20241030.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/RECORD +41 -40
  38. {streamlit_nightly-1.39.1.dev20241030.data → streamlit_nightly-1.39.1.dev20241101.data}/scripts/streamlit.cmd +0 -0
  39. {streamlit_nightly-1.39.1.dev20241030.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/WHEEL +0 -0
  40. {streamlit_nightly-1.39.1.dev20241030.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/entry_points.txt +0 -0
  41. {streamlit_nightly-1.39.1.dev20241030.dist-info → streamlit_nightly-1.39.1.dev20241101.dist-info}/top_level.txt +0 -0
@@ -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.image import AtomicImage, WidthBehaviour, image_to_url
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=WidthBehaviour.AUTO,
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=WidthBehaviour.AUTO,
158
+ width=WidthBehavior.AUTO,
159
159
  clamp=False,
160
160
  channels="RGB",
161
161
  output_format="auto",
@@ -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
- >>> pg = st.navigation([st.Page(page1), st.Page("page2.py")])
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
- Use a dictionary to create sections within your navigation menu.
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 image
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[image.AtomicImage, str]
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 image.image_to_url(
110
+ return image_to_url(
111
111
  page_icon,
112
112
  width=-1, # Always use full width for favicons
113
113
  clamp=False,
@@ -21,69 +21,26 @@
21
21
 
22
22
  from __future__ import annotations
23
23
 
24
- import io
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 means use the image width,
122
- but do not exceed the width of the column.
123
- Should be set for SVG images, as they have no default image width.
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 the figure's native width with the width of the
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 of the image to its natural width, up to
156
- the width of the parent container.
157
- Note: if `use_container_width` is set to `True`, it will take
158
- precedence over the `width` parameter
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
- WidthBehaviour.ORIGINAL if (width is None or width <= 0) else width
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 = WidthBehaviour.AUTO
144
+ image_width = WidthBehavior.AUTO
189
145
  elif use_column_width == "always" or use_column_width is True:
190
- image_width = WidthBehaviour.COLUMN
146
+ image_width = WidthBehavior.COLUMN
191
147
  elif use_column_width == "never" or use_column_width is False:
192
- image_width = WidthBehaviour.ORIGINAL
148
+ image_width = WidthBehavior.ORIGINAL
193
149
 
194
150
  else:
195
151
  if use_container_width is True:
196
- image_width = WidthBehaviour.MAX_IMAGE_OR_CONTAINER
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 = WidthBehaviour.MIN_IMAGE_OR_CONTAINER
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
- )