interactive_image.py 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. from __future__ import annotations
  2. import time
  3. from pathlib import Path
  4. from typing import Any, Callable, List, Optional, Union, cast
  5. from .. import optional_features
  6. from ..events import GenericEventArguments, MouseEventArguments, handle_event
  7. from .image import pil_to_base64
  8. from .mixins.content_element import ContentElement
  9. from .mixins.source_element import SourceElement
  10. try:
  11. from PIL.Image import Image as PIL_Image
  12. optional_features.register('pillow')
  13. except ImportError:
  14. pass
  15. class InteractiveImage(SourceElement, ContentElement, component='interactive_image.js'):
  16. CONTENT_PROP = 'content'
  17. PIL_CONVERT_FORMAT = 'PNG'
  18. def __init__(self,
  19. source: Union[str, Path, 'PIL_Image'] = '', *,
  20. content: str = '',
  21. on_mouse: Optional[Callable[..., Any]] = None,
  22. events: List[str] = ['click'],
  23. cross: bool = False,
  24. ) -> None:
  25. """Interactive Image
  26. Create an image with an SVG overlay that handles mouse events and yields image coordinates.
  27. It is also the best choice for non-flickering image updates.
  28. If the source URL changes faster than images can be loaded by the browser, some images are simply skipped.
  29. Thereby repeatedly updating the image source will automatically adapt to the available bandwidth.
  30. See `OpenCV Webcam <https://github.com/zauberzeug/nicegui/tree/main/examples/opencv_webcam/main.py>`_ for an example.
  31. :param source: the source of the image; can be an URL, local file path or a base64 string
  32. :param content: SVG content which should be overlaid; viewport has the same dimensions as the image
  33. :param on_mouse: callback for mouse events (yields `type`, `image_x` and `image_y`)
  34. :param events: list of JavaScript events to subscribe to (default: `['click']`)
  35. :param cross: whether to show crosshairs (default: `False`)
  36. """
  37. super().__init__(source=source, content=content)
  38. self._props['events'] = events
  39. self._props['cross'] = cross
  40. def handle_mouse(e: GenericEventArguments) -> None:
  41. if on_mouse is None:
  42. return
  43. args = cast(dict, e.args)
  44. arguments = MouseEventArguments(
  45. sender=self,
  46. client=self.client,
  47. type=args.get('mouse_event_type', ''),
  48. image_x=args.get('image_x', 0.0),
  49. image_y=args.get('image_y', 0.0),
  50. button=args.get('button', 0),
  51. buttons=args.get('buttons', 0),
  52. alt=args.get('alt', False),
  53. ctrl=args.get('ctrl', False),
  54. meta=args.get('meta', False),
  55. shift=args.get('shift', False),
  56. )
  57. handle_event(on_mouse, arguments)
  58. self.on('mouse', handle_mouse)
  59. def _set_props(self, source: Union[str, Path, 'PIL_Image']) -> None:
  60. if optional_features.has('pillow') and isinstance(source, PIL_Image):
  61. source = pil_to_base64(source, self.PIL_CONVERT_FORMAT)
  62. super()._set_props(source)
  63. def force_reload(self) -> None:
  64. """Force the image to reload from the source."""
  65. self._props['t'] = time.time()
  66. self.update()