64 lines
2.2 KiB
Python
64 lines
2.2 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import time
|
|
from contextlib import suppress
|
|
from pathlib import Path
|
|
|
|
from .. import optional_features
|
|
from ..logging import log
|
|
from .mixins.source_element import SourceElement
|
|
|
|
with suppress(ImportError):
|
|
from PIL.Image import Image as PIL_Image
|
|
optional_features.register('pillow')
|
|
|
|
|
|
class Image(SourceElement, component='image.js'):
|
|
PIL_CONVERT_FORMAT = 'PNG'
|
|
|
|
def __init__(self, source: str | Path | PIL_Image = '') -> None:
|
|
"""Image
|
|
|
|
Displays an image.
|
|
This element is based on Quasar's `QImg <https://quasar.dev/vue-components/img>`_ component.
|
|
|
|
:param source: the source of the image; can be a URL, local file path, a base64 string or a PIL image
|
|
"""
|
|
super().__init__(source=source)
|
|
|
|
def set_source(self, source: str | Path | PIL_Image) -> None:
|
|
return super().set_source(source)
|
|
|
|
def _set_props(self, source: str | Path | PIL_Image) -> None:
|
|
if optional_features.has('pillow') and isinstance(source, PIL_Image):
|
|
source = pil_to_tempfile(source, self.PIL_CONVERT_FORMAT)
|
|
super()._set_props(source)
|
|
|
|
def force_reload(self) -> None:
|
|
"""Force the image to reload from the source."""
|
|
if self._props['src'].startswith('data:'):
|
|
log.warning('ui.image: force_reload() only works with network sources (not data URIs)')
|
|
return
|
|
self._props['t'] = time.time()
|
|
|
|
|
|
def pil_to_tempfile(pil_image: PIL_Image, image_format: str) -> _TempPath:
|
|
"""Save a PIL image to a temporary file.
|
|
|
|
:param pil_image: the PIL image
|
|
:param image_format: the image format
|
|
:return: the path to the temporary file (auto-deletes when dereferenced)
|
|
"""
|
|
suffix = f'.{image_format.lower()}'
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
|
|
pil_image.save(temp_file, image_format)
|
|
return _TempPath(temp_file.name)
|
|
|
|
|
|
class _TempPath(type(Path())): # type: ignore[misc] # NOTE: Path is not subclassable before Python 3.12
|
|
"""A Path subclass that deletes itself when garbage collected."""
|
|
|
|
def __del__(self) -> None:
|
|
self.unlink(missing_ok=True)
|