Repository: wagtail/wagtail

Issue:
[Nightly] Image upload broken for some images
### Issue Summary

In the current Nightly version, it is no longer possible for me to upload an image. Maybe it is already known or fixed, but I preferred to report it.

```text
Internal Server Error: /admin/images/multiple/add/
Traceback (most recent call last):
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/views/decorators/cache.py", line 62, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/wagtail/admin/urls/__init__.py", line 170, in wrapper
    return view_func(request, *args, **kwargs)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/wagtail/admin/auth.py", line 205, in decorated_view
    return view_func(request, *args, **kwargs)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/views/generic/base.py", line 103, in view
    return self.dispatch(request, *args, **kwargs)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/utils/decorators.py", line 46, in _wrapper
    return bound_method(*args, **kwargs)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/views/decorators/vary.py", line 21, in inner_func
    response = func(*args, **kwargs)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/wagtail/admin/views/generic/multiple_upload.py", line 44, in dispatch
    return super().dispatch(request)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/wagtail/admin/views/generic/permissions.py", line 36, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/views/generic/base.py", line 142, in dispatch
    return handler(request, *args, **kwargs)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/wagtail/admin/views/generic/multiple_upload.py", line 144, in post
    if form.is_valid():
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/forms/forms.py", line 205, in is_valid
    return self.is_bound and not self.errors
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/forms/forms.py", line 200, in errors
    self.full_clean()
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/forms/forms.py", line 437, in full_clean
    self._clean_fields()
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/forms/forms.py", line 447, in _clean_fields
    value = field.clean(value, bf.initial)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/forms/fields.py", line 678, in clean
    return super().clean(data)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/django/forms/fields.py", line 198, in clean
    value = self.to_python(value)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/wagtail/images/fields.py", line 156, in to_python
    self.check_image_pixel_size(f)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/wagtail/images/fields.py", line 107, in check_image_pixel_size
    width, height = f.image.get_size()
  File "/wagtailNightly/.env/lib/python3.10/site-packages/willow/image.py", line 78, in wrapper
    image = converter(image)
  File "/wagtailNightly/.env/lib/python3.10/site-packages/willow/plugins/pillow.py", line 248, in open
    image_file.f.seek(0)
AttributeError: 'str' object has no attribute 'seek'
[01/Nov/2022 10:28:08] "POST /admin/images/multiple/add/ HTTP/1.1" 500 150299
```

### Steps to Reproduce

1. Start a new project with `wagtail start myproject`
2. Install the nightly version `pip install https://releases.wagtail.org/nightly/dist/wagtail-4.2.dev20221101-py3-none-any.whl`
3. Start the server and open the wagtail admin interface
4. Click on `Images` in the left sidebar
5. Upload an image

Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?

-   I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes

### Technical details

-   Python version: 3.10.8
-   Django version: 4.1.1
-   Wagtail version: 4.2.dev20221101
-   Browser version: Firefox 106 and Chrome 107



Discussion:
Thank you @vorlif 

#8974 made changes to the image field used under the hood.
I tested this locally and did not encounter any issues. One for me to double check.
@zerolab Thanks for the quick reply. After your feedback I tried a few other pictures and many of them were working. Here once a picture which did not go. One time compressed in case Github converts the images.

With version 4.0 and 4.1rc1 the image could still be uploaded. The pictures that don't work are, I believe, all from the same smartphone. (Samsung Galaxy S8)

![20210808_204140](https://user-images.githubusercontent.com/4333770/199217772-86ffab5c-df55-475c-969e-9862109fd920.jpg)
[20210808_204140.zip](https://github.com/wagtail/wagtail/files/9909159/20210808_204140.zip)


Resolving patch:
diff --git a/wagtail/images/fields.py b/wagtail/images/fields.py
--- a/wagtail/images/fields.py
+++ b/wagtail/images/fields.py
@@ -124,10 +124,11 @@ def to_python(self, data):
         if f is None:
             return None
 
-        # We need to get a file object for Pillow. We might have a path or we might
-        # have to read the data into memory.
+        # We need to get a file object for Pillow. When we get a path, we need to open
+        # the file first. And we have to read the data into memory to pass to Willow.
         if hasattr(data, "temporary_file_path"):
-            file = data.temporary_file_path()
+            with open(data.temporary_file_path(), "rb") as fh:
+                file = BytesIO(fh.read())
         else:
             if hasattr(data, "read"):
                 file = BytesIO(data.read())


Test patch:
diff --git a/wagtail/images/tests/tests.py b/wagtail/images/tests/tests.py
--- a/wagtail/images/tests/tests.py
+++ b/wagtail/images/tests/tests.py
@@ -3,11 +3,13 @@
 
 from django import forms, template
 from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
+from django.core.exceptions import ImproperlyConfigured, ValidationError
+from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
 from django.test import TestCase, override_settings
 from django.test.signals import setting_changed
 from django.urls import reverse
 from taggit.forms import TagField, TagWidget
+from willow.image import ImageFile as WillowImageFile
 
 from wagtail.images import get_image_model, get_image_model_string
 from wagtail.images.fields import WagtailImageField
@@ -566,7 +568,7 @@ def test_sendfile_dummy_backend(self):
         )
 
         self.assertEqual(response.status_code, 200)
-        self.assertTrue(response.content, "Dummy backend response")
+        self.assertTrue(response.content, msg="Dummy backend response")
 
 
 class TestRect(TestCase):
@@ -826,3 +828,60 @@ def test_invalid_get_image_model(self):
         """Test get_image_model with an invalid model string"""
         with self.assertRaises(ImproperlyConfigured):
             get_image_model()
+
+
+class TestWagtailImageField(TestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.filename = "wagtailimagefield.png"
+        cls.image = get_test_image_file(filename=cls.filename).file
+        cls.image_size = cls.image.getbuffer().nbytes
+
+    def test_to_python_with_inmemoryfile(self):
+        f = WagtailImageField()
+        self.image.seek(0)
+        file = InMemoryUploadedFile(
+            self.image, "", self.filename, "image/png", self.image_size, None
+        )
+        to_python = f.to_python(file)
+        self.assertIsInstance(to_python.image, WillowImageFile)
+        self.assertEqual(to_python.content_type, "image/png")
+
+    def test_to_python_gets_content_type_from_willow(self):
+        f = WagtailImageField()
+        self.image.seek(0)
+        file = InMemoryUploadedFile(
+            self.image, "", self.filename, "image/jpeg", self.image_size, None
+        )
+        to_python = f.to_python(file)
+        self.assertIsInstance(to_python.image, WillowImageFile)
+        self.assertEqual(to_python.content_type, "image/png")
+
+    def test_to_python_with_temporary_file(self):
+        f = WagtailImageField()
+        with TemporaryUploadedFile(
+            "test_temp.png", "image/png", self.image_size, None
+        ) as tmp_file:
+            self.image.seek(0)
+            tmp_file.write(self.image.read())
+            tmp_file.seek(0)
+
+            to_python = f.to_python(tmp_file)
+            self.assertIsInstance(to_python.image, WillowImageFile)
+            self.assertEqual(to_python.content_type, "image/png")
+
+    def test_to_python_raises_error_with_invalid_image_file(self):
+        msg = (
+            "Upload a valid image. The file you uploaded was either not an "
+            "image or a corrupted image."
+        )
+        f = WagtailImageField()
+        with TemporaryUploadedFile("test_temp.png", "image/png", 32, None) as tmp_file:
+            with self.assertRaisesMessage(ValidationError, msg):
+                f.to_python(tmp_file)
+
+        with self.assertRaisesMessage(
+            ValidationError,
+            "No file was submitted. Check the encoding type on the form.",
+        ):
+            f.to_python(self.image)