kopia lustrzana https://github.com/collective/icalendar
Add Image class as read attribute
rodzic
b0d98b004f
commit
140e50746d
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue