Add Image class as read attribute

pull/877/head
Nicco Kunzmann 2025-09-20 21:00:31 +01:00
rodzic b0d98b004f
commit 140e50746d
10 zmienionych plików z 256 dodań i 9 usunięć

Wyświetl plik

@ -51,7 +51,6 @@ from icalendar.parser import (
# Property Data Value Types
from icalendar.prop import (
Image,
TypesFactory,
vBinary,
vBoolean,
@ -75,6 +74,7 @@ from icalendar.prop import (
vUTCOffset,
vWeekday,
)
from icalendar.prop.image import Image
# Switching the timezone provider
from icalendar.timezone import use_pytz, use_zoneinfo

Wyświetl plik

@ -10,6 +10,7 @@ from icalendar.enums import BUSYTYPE, CLASS, STATUS, TRANSP, StrEnum
from icalendar.error import IncompleteComponent, InvalidCalendar
from icalendar.parser_tools import SEQUENCE_TYPES
from icalendar.prop import vCalAddress, vCategory, vDDDTypes, vDuration, vRecur, vText
from icalendar.prop.image import Image
from icalendar.timezone import tzp
from icalendar.tools import is_date
@ -1585,6 +1586,37 @@ def set_duration_with_locking(component, duration, locked, end_property):
raise ValueError(f"locked must be 'start' or 'end', not {locked!r}")
def _get_images(self: Component) -> list[Image]:
"""IMAGE specifies an image associated with the calendar or a calendar component.
Description:
This property specifies an image for an iCalendar
object or a calendar component via a URI or directly with inline
data that can be used by calendar user agents when presenting the
calendar data to a user. Multiple properties MAY be used to
specify alternative sets of images with, for example, varying
media subtypes, resolutions, or sizes. When multiple properties
are present, calendar user agents SHOULD display only one of them,
picking one that provides the most appropriate image quality, or
display none. The "DISPLAY" parameter is used to indicate the
intended display mode for the image. The "ALTREP" parameter,
defined in :rfc:`5545`, can be used to provide a "clickable" image
where the URI in the parameter value can be "launched" by a click
on the image in the calendar user agent.
Conformance:
This property can be specified multiple times in an
iCalendar object or in "VEVENT", "VTODO", or "VJOURNAL" calendar
components.
"""
images = self.get("IMAGE", [])
if not isinstance(images, SEQUENCE_TYPES):
images = [images]
return [Image.from_property_value(img) for img in images]
images_property = property(_get_images)
__all__ = [
"attendees_property",
"busy_type_property",
@ -1598,6 +1630,7 @@ __all__ = [
"descriptions_property",
"duration_property",
"exdates_property",
"images_property",
"location_property",
"multi_language_text_property",
"organizer_property",

Wyświetl plik

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Sequence
from icalendar.attr import (
categories_property,
images_property,
multi_language_text_property,
single_string_property,
source_property,
@ -495,6 +496,8 @@ Description:
"""Delete REFRESH-INTERVAL."""
self.pop("REFRESH-INTERVAL")
images = images_property
@classmethod
def new(
cls,

Wyświetl plik

@ -17,6 +17,7 @@ from icalendar.attr import (
create_single_property,
description_property,
exdates_property,
images_property,
location_property,
organizer_property,
priority_property,
@ -483,6 +484,7 @@ class Event(Component):
transparency = transparency_property
status = status_property
attendees = attendees_property
images = images_property
@classmethod
def new(

Wyświetl plik

@ -15,6 +15,7 @@ from icalendar.attr import (
create_single_property,
descriptions_property,
exdates_property,
images_property,
organizer_property,
rdates_property,
rrules_property,
@ -167,6 +168,8 @@ class Journal(Component):
"""Delete all descriptions."""
del self.descriptions
images = images_property
@classmethod
def new(
cls,

Wyświetl plik

@ -17,6 +17,7 @@ from icalendar.attr import (
create_single_property,
description_property,
exdates_property,
images_property,
location_property,
organizer_property,
priority_property,
@ -351,6 +352,7 @@ class Todo(Component):
contacts = contacts_property
status = status_property
attendees = attendees_property
images = images_property
@classmethod
def new(

Wyświetl plik

@ -63,8 +63,6 @@ from icalendar.parser_tools import (
from icalendar.timezone import tzid_from_dt, tzid_from_tzinfo, tzp
from icalendar.tools import to_datetime
from .image import Image
DURATION_REGEX = re.compile(
r"([-+]?)P(?:(\d+)W)?(?:(\d+)D)?" r"(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$"
)
@ -78,10 +76,13 @@ class vBinary:
"""Binary property values are base 64 encoded."""
params: Parameters
obj: str
def __init__(self, obj):
def __init__(self, obj, params: dict[str:str] | None = None):
self.obj = to_unicode(obj)
self.params = Parameters(encoding="BASE64", value="BINARY")
if params:
self.params.update(params)
def __repr__(self):
return f"vBinary({self.to_ical()})"
@ -2209,7 +2210,6 @@ class TypesFactory(CaselessDict):
__all__ = [
"DURATION_REGEX",
"WEEKDAY_RULE",
"Image",
"TimeBase",
"TypesFactory",
"tzid_from_dt",

Wyświetl plik

@ -1,8 +1,61 @@
"""This contains the IMAGE property from :rfc:`7986`."""
from __future__ import annotations
import base64
from icalendar.prop import vBinary, vText, vUri
class Image:
"""An image as URI or BINARY according to :rfc:`7986`."""
@classmethod
def from_property_value(cls, value: vUri | vBinary | vText):
"""Create an Image from a property value."""
params: dict[str, str] = {}
if not hasattr(value, "params"):
raise TypeError("Value must be URI or BINARY.")
value_type = value.params.get("VALUE", "").upper()
if value_type == "URI" or isinstance(value, vUri):
params["uri"] = str(value)
elif isinstance(value, vBinary):
params["b64data"] = value.obj
elif value_type == "BINARY":
params["b64data"] = str(value)
else:
raise TypeError(
f"The VALUE parameter must be URI or BINARY, not {value_type!r}."
)
params.update(
{
"fmttype": value.params.get("FMTTYPE"),
"altrep": value.params.get("ALTREP"),
"display": value.params.get("DISPLAY"),
}
)
return cls(**params)
def __init__(
self,
uri: str | None = None,
b64data: str | None = None,
fmttype: str | None = None,
altrep: str | None = None,
display: str | None = None,
):
self.uri = uri
self.b64data = b64data
self.fmttype = fmttype
self.altrep = altrep
self.display = display
@property
def data(self) -> bytes | None:
"""Return the binary data, if available."""
if self.b64data is None:
return None
return base64.b64decode(self.b64data)
__all__ = ["Image"]

Wyświetl plik

@ -5,10 +5,10 @@ IMAGE;VALUE=URI;DISPLAY=BADGE;FMTTYPE=image/png:http://example.com/images/party.
END:VEVENT
BEGIN:VTODO
UID:uri-todo
IMAGE;VALUE=URI;DISPLAY=BADGE;FMTTYPE=image/jpg:http://example.com/images/party.jpg
IMAGE;VALUE=URI;DISPLAY=BADGE:http://example.com/images/party.jpg
END:VTODO
BEGIN:VJOURNAL
UID:uri-todo
UID:uri-journal
IMAGE;VALUE=URI;DISPLAY=ICON;FMTTYPE=image/jpg:http://example.com/images/party.jpg
END:VJOURNAL
BEGIN:VEVENT
@ -17,7 +17,7 @@ IMAGE;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png:iVBORw0KGgoAAAANSUhEUgAAAAE
END:VEVENT
BEGIN:VTODO
UID:data-todo
IMAGE;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png;ALTREP=http://example.com/images/party.jpg:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII=
IMAGE;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png;ALTREP="http://example.com/images/party.jpg":iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII=
END:VTODO
BEGIN:VJOURNAL
UID:data-journal

Wyświetl plik

@ -1,8 +1,159 @@
"""Test the image class to convert from and to binary data."""
from __future__ import annotations
import base64
import pytest
from icalendar import Calendar, Image, vBinary, vUri
from icalendar.prop import vText
TRANSPARENT_PIXEL = base64.b64decode("""iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCA
YAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jn
m+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII=""")
@pytest.fixture
def images(calendars):
def images(calendars) -> dict[str, Image]:
"""Return the images we get from the example calendars."""
calendar: Calendar = calendars.rfc_7986_image
images: dict[str, Image] = {}
for component in calendar.subcomponents:
img = component.images[0]
images[component.uid] = img
return images
@pytest.mark.parametrize(
("uid", "uri", "data", "fmt_type", "altrep", "display"),
[
(
"uri-event",
"http://example.com/images/party.png",
None,
"image/png",
None,
"BADGE",
),
(
"uri-todo",
"http://example.com/images/party.jpg",
None,
None,
None,
"BADGE",
),
(
"uri-journal",
"http://example.com/images/party.jpg",
None,
"image/jpg",
None,
"ICON",
),
(
"data-event",
None,
TRANSPARENT_PIXEL,
"image/png",
None,
None,
),
(
"data-todo",
None,
TRANSPARENT_PIXEL,
"image/png",
"http://example.com/images/party.jpg",
None,
),
(
"data-journal",
None,
TRANSPARENT_PIXEL,
"image/png",
None,
"BADGE",
),
],
)
def test_image_parsing(
images,
uid,
uri,
data,
fmt_type,
altrep,
display,
):
"""Test that the image property is parsed correctly."""
image = images[uid]
assert image.uri == uri
assert image.data == data
assert image.fmttype == fmt_type
assert image.altrep == altrep
assert image.display == display
def test_no_images():
"""Test that an empty calendar has no images."""
calendar = Calendar()
assert len(calendar.images) == 0
def test_create_image_invalid_type():
"""Test that creating an image with invalid type raises TypeError."""
with pytest.raises(TypeError):
Image.from_property_value("not a valid type")
with pytest.raises(TypeError):
Image.from_property_value(vText("not a valid type", params={"VALUE": "TEXT"}))
with pytest.raises(TypeError):
Image.from_property_value(vText("http://example.com/image.png"))
def test_create_with_vBinary():
"""Test creating an Image from a vBinary property."""
b64data = base64.b64encode(TRANSPARENT_PIXEL).decode("ascii")
vbin = vBinary(b64data, params={"FMTTYPE": "image/png"})
image = Image.from_property_value(vbin)
assert image.uri is None
assert image.data == TRANSPARENT_PIXEL
assert image.fmttype == "image/png"
assert image.altrep is None
assert image.display is None
def test_create_with_vUri():
"""Test creating an Image from a vUri property."""
uri = "http://example.com/image.png"
vuri = vUri(uri, params={"FMTTYPE": "image/png", "DISPLAY": "BADGE"})
image = Image.from_property_value(vuri)
assert image.uri == uri
assert image.data is None
assert image.fmttype == "image/png"
assert image.altrep is None
assert image.display == "BADGE"
def test_create_image_with_vText_as_uri():
"""Test that creating an image with vText but VALUE URI or BINARY raises TypeError."""
img = Image.from_property_value(
vText("http://example.com/image.png", params={"VALUE": "URI"})
)
assert img.uri == "http://example.com/image.png"
assert img.data is None
assert img.fmttype is None
assert img.altrep is None
assert img.display is None
def test_create_image_with_vText_as_binary():
"""Test that creating an image with vText but VALUE URI or BINARY raises TypeError."""
b64data = base64.b64encode(TRANSPARENT_PIXEL).decode("ascii")
img = Image.from_property_value(vText(b64data, params={"VALUE": "BINARY"}))
assert img.uri is None
assert img.data == TRANSPARENT_PIXEL
assert img.fmttype is None
assert img.altrep is None
assert img.display is None