Repository: getnikola/nikola

Issue:
Thumbnail images are with the same size like the source images
<!--
Before creating an issue:
* make sure you are using an up-to-date version of Nikola
* search for existing issues that might be related

Make sure to:
* provide information about your environment (below)
* include all the output you get, and any other information related to your problem

Nikola v7.6.4, as provided by Ubuntu, is NOT SUPPORTED.
If you are using this version, you should upgrade: https://getnikola.com/getting-started.html
-->

### Environment

**Python Version:**
Python 3.7.3
**Nikola Version:**
Nikola v8.0.3
**Operating System:**
openSuse Tumbleweed / Windows 10
### Description:
After i run `nikola build` it creates new thumbnail images, but with the same size like the source images. If you want, you can check it by yourself with the images from my photo blog repository [1]. 

> -rw-r--r-- 1 marcus users 21771945 Dec 22 14:04 _63_1020.JPG
-rw-r--r-- 1 marcus users 21771945 Dec 22 14:04 _63_1020.thumbnail.JPG
-rw-r--r-- 1 marcus users 21936578 Dec 22 14:04 _63_1021.JPG
-rw-r--r-- 1 marcus users 21936578 Dec 22 14:04 _63_1021.thumbnail.JPG
-rw-r--r-- 1 marcus users 10862571 Dec 22 13:59 _DSC0849.JPG
-rw-r--r-- 1 marcus users 10862571 Dec 22 13:59 _DSC0849.thumbnail.JPG
-rw-r--r-- 1 marcus users  9256726 Dec 22 13:59 _DSC0850.JPG
-rw-r--r-- 1 marcus users  9256726 Dec 22 13:59 _DSC0850.thumbnail.JPG
-rw-r--r-- 1 marcus users 10139398 Dec 22 13:59 _DSC0851.JPG
-rw-r--r-- 1 marcus users 10139398 Dec 22 13:59 _DSC0851.thumbnail.JPG
-rw-r--r-- 1 marcus users 10533825 Dec 22 13:59 _DSC0852.JPG
-rw-r--r-- 1 marcus users 10533825 Dec 22 13:59 _DSC0852.thumbnail.JPG
-rw-r--r-- 1 marcus users   105627 Dec 22 14:03 IMG_0204.jpg
-rw-r--r-- 1 marcus users     3781 Dec 22 14:03 IMG_0204.thumbnail.jpg
-rw-r--r-- 1 marcus users   120466 Dec 22 14:03 IMG_0205.jpg
-rw-r--r-- 1 marcus users     3541 Dec 22 14:03 IMG_0205.thumbnail.jpg
-rw-r--r-- 1 marcus users   129014 Dec 22 14:03 IMG_0206.jpg
-rw-r--r-- 1 marcus users     4713 Dec 22 14:03 IMG_0206.thumbnail.jpg
-rw-r--r-- 1 marcus users   129206 Dec 22 14:03 IMG_0207.jpg
-rw-r--r-- 1 marcus users     4826 Dec 22 14:03 IMG_0207.thumbnail.jpg
-rw-r--r-- 1 marcus users     6481 Dec 22 14:04 index.html
-rw-r--r-- 1 marcus users     4129 Dec 22 14:04 rss.xml

[1] My personal photo blog https://gitlab.com/marcuskammer/photoblog/tree/nikola/photoblog/galleries/travel


Discussion:
This was discussed on IRC last month, ~~and it was determined to be caused by EXIF metadata in the photos:~~ (not really, see post below)

Log: https://irclogs.getnikola.com/logs/2019-11-13/
I did some debugging. The images that fail to be processed are detected as`<class 'PIL.MpoImagePlugin.MpoImageFile'>` with `is_animated = True, n_frames = 2`. Nikola refuses to resize animated images.

I’m not sure where to go from there and what we should do to those files.
Wow, thank you for your debugging. How can these images be classified as `animated`? Their are just regular jpegs, straight copied from my camera?! I did check the exif metadata with exiv2 and could not find any information about `animation`. Maybe it is a bug in the Python Lib Pillow? I will play around with Pillow.
I did check the jpeg standard. https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
It is allowed to embed a thumbnail image into the Exif metadata of a jpeg image. Many camera producers do this to allow a preview of the image. The related exif metadata are `Exif.Thumbnail.*`. Because of this, PILLOW identifies these images as Multi Picture Object and not as `JpegImageFile`.

I also did check `image_processing.py`:
```
def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False):
        """Make a copy of the image in the requested size."""
        if not Image or os.path.splitext(src)[1] in ['.svg', '.svgz']:
            self.resize_svg(src, dst, max_size, bigger_panoramas)
            return
        im = Image.open(src)

        if hasattr(im, 'n_frames') and im.n_frames > 1:
            # Animated gif, leave as-is
            utils.copy_file(src, dst)
            return
```

It checks if the image has more than one frame. And of course it has 2 frames, the original image and the embedded thumbnail image. But it is wrong to suggest a image is of type GIF if the frame count is greater than 1?!
Pillow also says that this image `is_animated`, although we don’t check that attribute.  I think we could override this check for MPO/JPEG-with-thumbnail files, but you should perhaps report this issue with `is_animated/n_frames` directly to Pillow (https://github.com/python-pillow/Pillow/issues).
I do not know the intention of `if hasattr(im, 'n_frames') and im.n_frames > 1:` but as the comment line suggests, it should check if the image is of type Gif. I would suggest the use of isinstance instead of hasattr. e.g.:

```
from PIL.GifImagePlugin import GifImageFile
if isinstance(im, GifImageFile):
    ...
```
The current implementation has an issue with your images, but handles two other cases correctly: (a) non-animated GIFs can be scaled, and (b) animated images don’t have to be GIFs.
It is getting more clear to me, thank you!. 
I guess, pillow should not handle these jpeg images, which includes a thumbnail, as MPO.
Since there is no such thing as an animated JPEG, I will special-case that.

Resolving patch:
diff --git a/nikola/image_processing.py b/nikola/image_processing.py
--- a/nikola/image_processing.py
+++ b/nikola/image_processing.py
@@ -83,13 +83,15 @@ def filter_exif(self, exif, whitelist):
 
     def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False):
         """Make a copy of the image in the requested size."""
-        if os.path.splitext(src)[1] in ['.svg', '.svgz']:
+        extension = os.path.splitext(src)[1].lower()
+        if extension in {'.svg', '.svgz'}:
             self.resize_svg(src, dst, max_size, bigger_panoramas)
             return
 
         im = Image.open(src)
 
-        if hasattr(im, 'n_frames') and im.n_frames > 1:
+        # The jpg exclusion is Issue #3332
+        if hasattr(im, 'n_frames') and im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}:
             # Animated gif, leave as-is
             utils.copy_file(src, dst)
             return


Test patch: