API: Add image support #199 (#200)

This commit is contained in:
Daniel Grams 2021-06-23 16:04:06 +02:00 committed by GitHub
parent a786c65119
commit 238f581baa
18 changed files with 441 additions and 37 deletions

View File

@ -8,8 +8,12 @@ from project.api.event_category.schemas import (
EventCategoryRefSchema,
EventCategoryWriteIdSchema,
)
from project.api.fields import CustomDateTimeField
from project.api.image.schemas import ImageSchema
from project.api.fields import CustomDateTimeField, Owned
from project.api.image.schemas import (
ImagePatchRequestSchema,
ImagePostRequestSchema,
ImageSchema,
)
from project.api.organization.schemas import OrganizationRefSchema
from project.api.organizer.schemas import OrganizerRefSchema, OrganizerWriteIdSchema
from project.api.place.schemas import (
@ -260,7 +264,7 @@ class EventWriteSchemaMixin(object):
default=50,
validate=validate.OneOf([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]),
metadata={
"description": "How relevant the event is to your organization. 0 (Little relevant), 5 (Default), 10 (Highlight)."
"description": "How relevant the event is to your organization. 0 (Little relevant), 50 (Default), 100 (Highlight)."
},
)
@ -272,6 +276,8 @@ class EventPostRequestSchema(
super().__init__(*args, **kwargs)
self.make_post_schema()
photo = Owned(ImagePostRequestSchema)
class EventPatchRequestSchema(
EventModelSchema, EventBaseSchemaMixin, EventWriteSchemaMixin
@ -279,3 +285,5 @@ class EventPatchRequestSchema(
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_patch_schema()
photo = Owned(ImagePatchRequestSchema)

View File

@ -1,4 +1,5 @@
from marshmallow import ValidationError, fields
from marshmallow_sqlalchemy import fields as msfields
from project.dateutils import berlin_tz
@ -31,3 +32,16 @@ class CustomDateTimeField(fields.DateTime):
result = berlin_tz.localize(result)
return result
class Owned(msfields.Nested):
def _deserialize(self, *args, **kwargs):
if (
not self.root.transient
and hasattr(self.schema, "instance")
and self.schema.instance is None
and self.root.instance
and hasattr(self.root.instance, self.name)
):
self.schema.instance = getattr(self.root.instance, self.name, None)
return super()._deserialize(*args, **kwargs)

View File

@ -1,20 +1,33 @@
from marshmallow import ValidationError, fields, post_load, validates_schema
from project.api import marshmallow
from project.api.schemas import IdSchemaMixin, SQLAlchemyBaseSchema
from project.imageutils import (
get_bytes_from_image,
get_image_from_base64_str,
get_image_from_url,
get_mime_type_from_image,
resize_image_to_max,
validate_image,
)
from project.models import Image
class ImageIdSchema(marshmallow.SQLAlchemySchema):
class ImageModelSchema(SQLAlchemyBaseSchema):
class Meta:
model = Image
load_instance = True
id = marshmallow.auto_field()
class ImageIdSchema(ImageModelSchema, IdSchemaMixin):
pass
class ImageBaseSchema(ImageIdSchema):
class ImageBaseSchemaMixin(object):
copyright_text = marshmallow.auto_field()
class ImageSchema(ImageBaseSchema):
class ImageSchema(ImageIdSchema, ImageBaseSchemaMixin):
image_url = marshmallow.URLFor(
"image",
values=dict(id="<id>", s=500),
@ -24,5 +37,81 @@ class ImageSchema(ImageBaseSchema):
)
class ImageDumpSchema(ImageBaseSchema):
class ImageDumpSchema(ImageModelSchema, ImageBaseSchemaMixin):
pass
class ImageWriteSchemaMixin(object):
image_url = fields.String(
required=False,
load_only=True,
allow_none=True,
metadata={
"description": "URL to image. Either image_url or image_base64 has to be defined."
},
)
image_base64 = fields.String(
required=False,
load_only=True,
allow_none=True,
metadata={
"description": "Base64 encoded image data. Either image_url or image_base64 has to be defined."
},
)
@post_load(pass_original=True)
def post_load_image_data(self, item, original_data, **kwargs):
image_url = original_data.get("image_url")
image_base64 = original_data.get("image_base64")
if image_url is not None or image_base64 is not None:
# Mal ist item ein dict und mal ein Image, weil post_load die Reihenfolge der Aufrufe nicht garantieren kann
if isinstance(item, dict):
encoding_format, data = self.load_image_data(image_base64, image_url)
item["encoding_format"] = encoding_format
item["data"] = data
elif isinstance(item, Image):
encoding_format, data = self.load_image_data(image_base64, image_url)
item.encoding_format = encoding_format
item.data = data
return item
def load_image_data(self, image_base64, image_url):
image = None
if image_base64:
image = get_image_from_base64_str(image_base64)
elif image_url:
image = get_image_from_url(image_url)
if not image:
return None, None
validate_image(image)
resize_image_to_max(image)
encoding_format = get_mime_type_from_image(image)
data = get_bytes_from_image(image)
return encoding_format, data
class ImagePostRequestSchema(
ImageModelSchema, ImageBaseSchemaMixin, ImageWriteSchemaMixin
):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_post_schema()
@validates_schema
def validate_image(self, data, **kwargs):
if data.get("image_url") is None and data.get("image_base64") is None:
raise ValidationError("Either image_url or image_base64 has to be defined.")
class ImagePatchRequestSchema(
ImageModelSchema, ImageBaseSchemaMixin, ImageWriteSchemaMixin
):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_patch_schema()

View File

@ -1,6 +1,7 @@
from marshmallow import fields, validate
from project.api import marshmallow
from project.api.fields import Owned
from project.api.image.schemas import ImageSchema
from project.api.location.schemas import (
LocationPatchRequestSchema,
@ -78,7 +79,7 @@ class OrganizerPostRequestSchema(OrganizerModelSchema, OrganizerBaseSchemaMixin)
super().__init__(*args, **kwargs)
self.make_post_schema()
location = fields.Nested(LocationPostRequestSchema)
location = Owned(LocationPostRequestSchema)
class OrganizerPatchRequestSchema(OrganizerModelSchema, OrganizerBaseSchemaMixin):
@ -86,4 +87,4 @@ class OrganizerPatchRequestSchema(OrganizerModelSchema, OrganizerBaseSchemaMixin
super().__init__(*args, **kwargs)
self.make_patch_schema()
location = fields.Nested(LocationPatchRequestSchema)
location = Owned(LocationPatchRequestSchema)

View File

@ -1,6 +1,7 @@
from marshmallow import fields, validate
from project.api import marshmallow
from project.api.fields import Owned
from project.api.image.schemas import ImageSchema
from project.api.location.schemas import (
LocationPatchRequestSchema,
@ -79,7 +80,7 @@ class PlacePostRequestSchema(PlaceModelSchema, PlaceBaseSchemaMixin):
super().__init__(*args, **kwargs)
self.make_post_schema()
location = fields.Nested(LocationPostRequestSchema)
location = Owned(LocationPostRequestSchema)
class PlacePatchRequestSchema(PlaceModelSchema, PlaceBaseSchemaMixin):
@ -87,4 +88,4 @@ class PlacePatchRequestSchema(PlaceModelSchema, PlaceBaseSchemaMixin):
super().__init__(*args, **kwargs)
self.make_patch_schema()
location = fields.Nested(LocationPatchRequestSchema)
location = Owned(LocationPatchRequestSchema)

View File

@ -1,6 +1,3 @@
import base64
import re
from flask import url_for
from flask_babelex import lazy_gettext
from flask_wtf import FlaskForm
@ -8,6 +5,15 @@ from markupsafe import Markup
from wtforms import HiddenField, StringField
from wtforms.validators import Optional
from project.imageutils import (
get_bytes_from_image,
get_data_uri_from_bytes,
get_image_from_base64_str,
get_mime_type_from_image,
resize_image_to_max,
validate_image,
)
class BaseImageForm(FlaskForm):
copyright_text = StringField(
@ -22,22 +28,19 @@ class Base64ImageForm(BaseImageForm):
super(BaseImageForm, self).process(formdata, obj, data, **kwargs)
if self.image_base64.data is None and obj and obj.data:
base64_str = base64.b64encode(obj.data).decode("utf-8")
self.image_base64.data = "data:{};base64,{}".format(
obj.encoding_format, base64_str
self.image_base64.data = get_data_uri_from_bytes(
obj.data, obj.encoding_format
)
def populate_obj(self, obj):
super(BaseImageForm, self).populate_obj(obj)
match = None
if self.image_base64.data:
match = re.match(r"^data:(image/.+);base64,(.*)$", self.image_base64.data)
if match:
obj.encoding_format = match.group(1)
base64_str = match.group(2)
obj.data = base64.b64decode(base64_str)
image = get_image_from_base64_str(self.image_base64.data)
validate_image(image)
resize_image_to_max(image)
obj.encoding_format = get_mime_type_from_image(image)
obj.data = get_bytes_from_image(image)
else:
obj.data = None
obj.encoding_format = None

56
project/imageutils.py Normal file
View File

@ -0,0 +1,56 @@
import base64
import re
from io import BytesIO
import PIL
import requests
min_image_size = 320
max_image_size = 1024
supported_formats = ["jpeg", "png", "gif"]
def get_data_uri_from_bytes(data: bytes, encoding_format: str) -> str:
base64_str = base64.b64encode(data).decode("utf-8")
return "data:{};base64,{}".format(encoding_format, base64_str)
def get_image_from_bytes(data: bytes) -> PIL.Image:
return PIL.Image.open(BytesIO(data))
def get_image_from_base64_str(base64_str: str) -> PIL.Image:
image_data = re.sub("^data:image/.+;base64,", "", base64_str)
return get_image_from_bytes(base64.b64decode(image_data))
def get_image_from_url(url: str) -> PIL.Image:
response = requests.get(url)
return get_image_from_bytes(response.content)
def get_mime_type_from_image(image: PIL.Image) -> str:
return image.get_format_mimetype()
def get_bytes_from_image(image: PIL.Image) -> bytes:
imgByteArr = BytesIO()
image.save(imgByteArr, format=image.format)
return imgByteArr.getvalue()
def resize_image_to_max(image: PIL.Image):
if image.width > max_image_size or image.height > max_image_size:
image.thumbnail((max_image_size, max_image_size), PIL.Image.ANTIALIAS)
def validate_image(image: PIL.Image):
if image.width < min_image_size or image.height < min_image_size:
raise ValueError(
f"Image is too small ({image.width}x{image.height}px). At least {min_image_size}x{min_image_size}px."
)
if image.format.lower() not in supported_formats:
raise ValueError(
f"Image format {image.format} is not supported. Supported formats: {', '.join(supported_formats)}."
)

View File

@ -1,6 +1,8 @@
import os
from urllib.parse import quote_plus
from flask import url_for
from project import app
from project.utils import (
get_event_category_name,
@ -43,7 +45,7 @@ app.jinja_env.filters["location_str"] = lambda l: get_location_str(l)
@app.context_processor
def get_manage_menu_options_context_processor():
def get_context_processors():
def get_manage_menu_options(admin_unit):
from project.access import has_access
from project.services.event_suggestion import get_event_reviews_badge_query
@ -64,4 +66,9 @@ def get_manage_menu_options_context_processor():
"reference_requests_incoming_badge": reference_requests_incoming_badge,
}
return dict(get_manage_menu_options=get_manage_menu_options)
def get_base_url():
return url_for("home", _external=True).rstrip("/")
return dict(
get_manage_menu_options=get_manage_menu_options, get_base_url=get_base_url
)

View File

@ -299,9 +299,13 @@ class AdminUnit(db.Model, TrackableMixin):
)
event_places = relationship("EventPlace", backref=backref("adminunit", lazy=True))
location_id = deferred(db.Column(db.Integer, db.ForeignKey("location.id")))
location = db.relationship("Location")
location = db.relationship(
"Location", uselist=False, single_parent=True, cascade="all, delete-orphan"
)
logo_id = deferred(db.Column(db.Integer, db.ForeignKey("image.id")))
logo = db.relationship("Image", uselist=False)
logo = db.relationship(
"Image", uselist=False, single_parent=True, cascade="all, delete-orphan"
)
url = deferred(Column(String(255)), group="detail")
email = deferred(Column(Unicode(255)), group="detail")
phone = deferred(Column(Unicode(255)), group="detail")
@ -335,6 +339,9 @@ class Location(db.Model, TrackableMixin):
longitude = Column(Numeric(19, 16))
coordinate = Column(Geometry(geometry_type="POINT"))
def __init__(self, **kwargs):
super(Location, self).__init__(**kwargs)
def is_empty(self):
return (
not self.street
@ -382,9 +389,13 @@ class EventPlace(db.Model, TrackableMixin):
id = Column(Integer(), primary_key=True)
name = Column(Unicode(255), nullable=False)
location_id = db.Column(db.Integer, db.ForeignKey("location.id"))
location = db.relationship("Location", uselist=False)
location = db.relationship(
"Location", uselist=False, single_parent=True, cascade="all, delete-orphan"
)
photo_id = db.Column(db.Integer, db.ForeignKey("image.id"))
photo = db.relationship("Image", uselist=False)
photo = db.relationship(
"Image", uselist=False, single_parent=True, cascade="all, delete-orphan"
)
url = Column(String(255))
description = Column(UnicodeText())
admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=True)
@ -460,9 +471,13 @@ class EventOrganizer(db.Model, TrackableMixin):
phone = deferred(Column(Unicode(255)), group="detail")
fax = deferred(Column(Unicode(255)), group="detail")
location_id = deferred(db.Column(db.Integer, db.ForeignKey("location.id")))
location = db.relationship("Location")
location = db.relationship(
"Location", uselist=False, single_parent=True, cascade="all, delete-orphan"
)
logo_id = deferred(db.Column(db.Integer, db.ForeignKey("image.id")))
logo = db.relationship("Image", uselist=False)
logo = db.relationship(
"Image", uselist=False, single_parent=True, cascade="all, delete-orphan"
)
admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=True)
@ -539,7 +554,9 @@ class EventMixin(object):
@declared_attr
def photo(cls):
return relationship("Image", uselist=False)
return relationship(
"Image", uselist=False, single_parent=True, cascade="all, delete-orphan"
)
class EventSuggestion(db.Model, TrackableMixin, EventMixin):

View File

@ -4,7 +4,7 @@
{%- endblock -%}
{% block content %}
<event-date-search basepath="{{ url_for('home', _external=True) }}"></event-date-search>
<event-date-search basepath="{{ get_base_url() }}"></event-date-search>
<script type="text/javascript" src="{{ url_for('static', filename='angular-elements.js')}}"></script>
{% endblock %}

View File

@ -5,7 +5,7 @@
{% block content_container_attribs %}{% endblock %}
{% block content %}
<organization-landing-page basepath="{{ url_for('home', _external=True) }}" organizationId="{{ organization.id }}" {% if "r" in request.args %}datefilterpreset="{{ request.args.get("r") }}{% endif %}"></organization-landing-page>
<organization-landing-page basepath="{{ get_base_url() }}" organizationId="{{ organization.id }}" {% if "r" in request.args %}datefilterpreset="{{ request.args.get("r") }}{% endif %}"></organization-landing-page>
<script type="text/javascript" src="{{ url_for('static', filename='angular-elements.js')}}"></script>
{% endblock %}

View File

@ -91,6 +91,7 @@ PyYAML==5.4.1
qrcode==6.1
regex==2020.11.13
requests==2.25.0
requests-mock==1.9.3
requests-oauthlib==1.3.0
rope==0.18.0
six==1.15.0

View File

@ -1,3 +1,6 @@
import base64
def test_read(client, app, db, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
event_id = seeder.create_event(admin_unit_id)
@ -400,6 +403,86 @@ def test_patch_referencedEventUpdate_sendsMail(client, seeder, utils, app, mocke
utils.assert_send_mail_called(mail_mock, "other@test.de")
def test_patch_photo(client, seeder, utils, app, requests_mock):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
requests_mock.get(
"https://image.com", content=base64.b64decode(seeder.get_default_image_base64())
)
url = utils.get_url("api_v1_event", id=event_id)
response = utils.patch_json(
url,
{"photo": {"image_url": "https://image.com"}},
)
utils.assert_response_no_content(response)
with app.app_context():
from project.models import Event
event = Event.query.get(event_id)
assert event.photo is not None
assert event.photo.encoding_format == "image/png"
def test_patch_photo_copyright(client, db, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
image_id = seeder.upsert_default_image()
with app.app_context():
from project.models import Event
event = Event.query.get(event_id)
event.photo_id = image_id
db.session.commit()
url = utils.get_url("api_v1_event", id=event_id)
response = utils.patch_json(
url,
{"photo": {"copyright_text": "Heiner"}},
)
utils.assert_response_no_content(response)
with app.app_context():
from project.models import Event
event = Event.query.get(event_id)
assert event.photo.id == image_id
assert event.photo.data is not None
assert event.photo.copyright_text == "Heiner"
def test_patch_photo_delete(client, db, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
image_id = seeder.upsert_default_image()
with app.app_context():
from project.models import Event
event = Event.query.get(event_id)
event.photo_id = image_id
db.session.commit()
url = utils.get_url("api_v1_event", id=event_id)
response = utils.patch_json(
url,
{"photo": None},
)
utils.assert_response_no_content(response)
with app.app_context():
from project.models import Event, Image
event = Event.query.get(event_id)
assert event.photo_id is None
image = Image.query.get(image_id)
assert image is None
def test_delete(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)

44
tests/api/test_image.py Normal file
View File

@ -0,0 +1,44 @@
import pytest
def test_validate_image():
from marshmallow import ValidationError
from project.api.image.schemas import ImagePostRequestSchema
data = {
"copyright_text": "Horst",
}
schema = ImagePostRequestSchema()
with pytest.raises(ValidationError) as e:
schema.load(data)
assert "Either image_url or image_base64 has to be defined." in str(e.value)
def test_post_load_image_data(seeder):
from project.api.image.schemas import ImagePostRequestSchema
data = {
"image_base64": seeder.get_default_image_upload_base64(),
}
item = dict()
schema = ImagePostRequestSchema()
schema.post_load_image_data(item, data)
schema.load(data)
assert item.get("encoding_format") is not None
assert item.get("data") is not None
def test_load_image_data():
from project.api.image.schemas import ImagePostRequestSchema
schema = ImagePostRequestSchema()
encoding_format, data = schema.load_image_data(None, None)
assert encoding_format is None
assert data is None

View File

@ -80,6 +80,7 @@ def test_events_post(client, seeder, utils, app):
"start": "2021-02-07T11:00:00.000Z",
"place": {"id": place_id},
"organizer": {"id": organizer_id},
"photo": {"image_base64": seeder.get_default_image_upload_base64()},
},
)
utils.assert_response_created(response)
@ -96,6 +97,8 @@ def test_events_post(client, seeder, utils, app):
assert event is not None
assert event.event_place_id == place_id
assert event.organizer_id == organizer_id
assert event.photo is not None
assert event.photo.encoding_format == "image/png"
def test_places(client, seeder, utils):

View File

@ -53,6 +53,38 @@ def test_patch(client, seeder, utils, app):
assert place.description == "Klasse"
def test_patch_location(db, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
place_id = seeder.upsert_default_event_place(admin_unit_id)
with app.app_context():
from project.models import EventPlace, Location
location = Location()
location.postalCode = "12345"
location.city = "City"
event = EventPlace.query.get(place_id)
event.location = location
db.session.commit()
location_id = location.id
url = utils.get_url("api_v1_place", id=place_id)
response = utils.patch_json(
url,
{"location": {"postalCode": "54321"}},
)
utils.assert_response_no_content(response)
with app.app_context():
from project.models import EventPlace
place = EventPlace.query.get(place_id)
assert place.location.id == location_id
assert place.location.postalCode == "54321"
def test_delete(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
place_id = seeder.upsert_default_event_place(admin_unit_id)

View File

@ -250,7 +250,7 @@ class Seeder(object):
return response.json["id"]
def get_default_image_base64(self):
return """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"""
return """iVBORw0KGgoAAAANSUhEUgAAAUAAAAFABAMAAAA/vriZAAAAG1BMVEUAAAD///9fX1+fn5+/v7/f398/Pz8fHx9/f38YYyahAAAACXBIWXMAAA7EAAAOxAGVKw4bAAADAklEQVR4nO3XzVPaQBzG8RQw4dhosB6hdexVhPZM6lg9mlrGHkFH7RGmhfYIM53aP7v7lhBeNgqul/b7OZBNzOPu/DbZBc8DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8O97c9I97qlWqd39fO+lzbOBNSJvfO+tl9lU0AyPW3Ek//G3MDo+CSfyqp9E7WTPlhE3ttrhqbdOZmPlnZ4Y5fRSNJuyKDeqi/626DA+tGTUjT9rg3UyG/PVpPiR+HgrW0EiB6w6r9jKcac++511Mk8VZw9PQ9SgvKuaSeETVXmVNR+d2dy0l7ZenHvesKP7nRRFqrtZ89GZzc0GWHkpzuq6305RJDfAR2c2F891ZiY8ncRqTx9fz0XkjfaMY/5O1twS09U1AzM1GuljKZrL9A+LMo7dXGZN8SwFZiSlbX1Ub6l4vE7zEb87KMq4dR3NOmvUPd+sFelRl7AU5V9Qv5kb7qqMw9G1TqL67DSZdRKkE69KmCug32onH7zijDvTMLyYDbC6t6ozUcJ8AathWDvyHsg4NN6vZSMcnq/qTJRw/gkMruJPXnHGqe/Zox33VnY22osWtoggrj+QcSrdocpypCs6C5LLxchWuuLZMk6lO1Rzkusk90aWwqX1rZT+1ZZxyuxQVd3N8prW+KjXwjxzlzXzHAOc6t0h0RfL2bYlXuHRUgnNkGyZ5xigKYbX7KmD3ME08QoHSyXUA7Rm3BqqMjTN9tqf6EO626o1cLGE5qmzZRyb1j359d+cmRfUFMVsIoslrKqnzZpxK6jJj3hiTkuq1+zbi9lEFkool2d7xpmtuvy8kX2PZi+gmrdGuvQN9SYSmG9UntpDfFVQa8aZSu3o7vZXWJdTePZHuldXf4v9r2fu8c0q/sOcxxcH46+xHJo94851InZ+2UEQamoi98XFiS3ivxO3fRmslXmC4Op2xS+x8UHRz7Pxwd3Kq8/0kw4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4L/0FiGF8UcQrzsIAAAAASUVORK5CYII="""
def get_default_image_upload_base64(self):
base64_str = self.get_default_image_base64()

45
tests/test_imageutils.py Normal file
View File

@ -0,0 +1,45 @@
import pytest
def test_resize_image_to_max():
import PIL
from project.imageutils import max_image_size, resize_image_to_max
image = PIL.Image.new("RGB", (max_image_size + 1, max_image_size + 1))
resize_image_to_max(image)
assert image.width == max_image_size
assert image.height == max_image_size
def test_validate_image_too_small():
import PIL
from project.imageutils import min_image_size, validate_image
image = PIL.Image.new("RGB", (min_image_size - 1, min_image_size - 1))
with pytest.raises(ValueError) as e:
validate_image(image)
assert "too small" in str(e.value)
def test_validate_image_unsupported_format():
from io import BytesIO
import PIL
from project.imageutils import get_image_from_bytes, min_image_size, validate_image
image = PIL.Image.new("RGB", (min_image_size, min_image_size))
imgByteArr = BytesIO()
image.save(imgByteArr, format="TIFF")
tif_image = get_image_from_bytes(imgByteArr.getvalue())
with pytest.raises(ValueError) as e:
validate_image(tif_image)
assert "not supported" in str(e.value)