We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `nikola/image_processing.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 # Copyright © 2014 Roberto Alsina and others.
4 
5 # Permission is hereby granted, free of charge, to any
6 # person obtaining a copy of this software and associated
7 # documentation files (the "Software"), to deal in the
8 # Software without restriction, including without limitation
9 # the rights to use, copy, modify, merge, publish,
10 # distribute, sublicense, and/or sell copies of the
11 # Software, and to permit persons to whom the Software is
12 # furnished to do so, subject to the following conditions:
13 #
14 # The above copyright notice and this permission notice
15 # shall be included in all copies or substantial portions of
16 # the Software.
17 #
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
19 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
20 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
21 # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
22 # OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
23 # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
24 # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
25 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 
27 """Process images."""
28 
29 import datetime
30 import gzip
31 import os
32 import re
33 
34 import lxml
35 import piexif
36 from PIL import ExifTags, Image
37 
38 from nikola import utils
39 
40 EXIF_TAG_NAMES = {}
41 
42 
43 class ImageProcessor(object):
44     """Apply image operations."""
45 
46     image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff']
47 
48     def _fill_exif_tag_names(self):
49         """Connect EXIF tag names to numeric values."""
50         if not EXIF_TAG_NAMES:
51             for ifd in piexif.TAGS:
52                 for tag, data in piexif.TAGS[ifd].items():
53                     EXIF_TAG_NAMES[tag] = data['name']
54 
55     def filter_exif(self, exif, whitelist):
56         """Filter EXIF data as described in the documentation."""
57         # Scenario 1: keep everything
58         if whitelist == {'*': '*'}:
59             return exif
60 
61         # Scenario 2: keep nothing
62         if whitelist == {}:
63             return None
64 
65         # Scenario 3: keep some
66         self._fill_exif_tag_names()
67         exif = exif.copy()  # Don't modify in-place, it's rude
68         for k in list(exif.keys()):
69             if type(exif[k]) != dict:
70                 pass  # At least thumbnails have no fields
71             elif k not in whitelist:
72                 exif.pop(k)  # Not whitelisted, remove
73             elif k in whitelist and whitelist[k] == '*':
74                 # Fully whitelisted, keep all
75                 pass
76             else:
77                 # Partially whitelisted
78                 for tag in list(exif[k].keys()):
79                     if EXIF_TAG_NAMES[tag] not in whitelist[k]:
80                         exif[k].pop(tag)
81 
82         return exif or None
83 
84     def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False):
85         """Make a copy of the image in the requested size."""
86         if os.path.splitext(src)[1] in ['.svg', '.svgz']:
87             self.resize_svg(src, dst, max_size, bigger_panoramas)
88             return
89 
90         im = Image.open(src)
91 
92         if hasattr(im, 'n_frames') and im.n_frames > 1:
93             # Animated gif, leave as-is
94             utils.copy_file(src, dst)
95             return
96 
97         size = w, h = im.size
98         if w > max_size or h > max_size:
99             size = max_size, max_size
100 
101             # Panoramas get larger thumbnails because they look *awful*
102             if bigger_panoramas and w > 2 * h:
103                 size = min(w, max_size * 4), min(w, max_size * 4)
104 
105         try:
106             exif = piexif.load(im.info["exif"])
107         except KeyError:
108             exif = None
109         # Inside this if, we can manipulate exif as much as
110         # we want/need and it will be preserved if required
111         if exif is not None:
112             # Rotate according to EXIF
113             value = exif['0th'].get(piexif.ImageIFD.Orientation, 1)
114             if value in (3, 4):
115                 im = im.transpose(Image.ROTATE_180)
116             elif value in (5, 6):
117                 im = im.transpose(Image.ROTATE_270)
118             elif value in (7, 8):
119                 im = im.transpose(Image.ROTATE_90)
120             if value in (2, 4, 5, 7):
121                 im = im.transpose(Image.FLIP_LEFT_RIGHT)
122             exif['0th'][piexif.ImageIFD.Orientation] = 1
123 
124         try:
125             icc_profile = im.info.get('icc_profile') if preserve_icc_profiles else None
126             im.thumbnail(size, Image.ANTIALIAS)
127             if exif is not None and preserve_exif_data:
128                 # Put right size in EXIF data
129                 w, h = im.size
130                 if '0th' in exif:
131                     exif["0th"][piexif.ImageIFD.ImageWidth] = w
132                     exif["0th"][piexif.ImageIFD.ImageLength] = h
133                 if 'Exif' in exif:
134                     exif["Exif"][piexif.ExifIFD.PixelXDimension] = w
135                     exif["Exif"][piexif.ExifIFD.PixelYDimension] = h
136                 # Filter EXIF data as required
137                 exif = self.filter_exif(exif, exif_whitelist)
138                 im.save(dst, exif=piexif.dump(exif), icc_profile=icc_profile)
139             else:
140                 im.save(dst, icc_profile=icc_profile)
141         except Exception as e:
142             self.logger.warning("Can't process {0}, using original "
143                                 "image! ({1})".format(src, e))
144             utils.copy_file(src, dst)
145 
146     def resize_svg(self, src, dst, max_size, bigger_panoramas):
147         """Make a copy of an svg at the requested size."""
148         try:
149             # Resize svg based on viewport hacking.
150             # note that this can also lead to enlarged svgs
151             if src.endswith('.svgz'):
152                 with gzip.GzipFile(src, 'rb') as op:
153                     xml = op.read()
154             else:
155                 with open(src, 'rb') as op:
156                     xml = op.read()
157             tree = lxml.etree.XML(xml)
158             width = tree.attrib['width']
159             height = tree.attrib['height']
160             w = int(re.search("[0-9]+", width).group(0))
161             h = int(re.search("[0-9]+", height).group(0))
162             # calculate new size preserving aspect ratio.
163             ratio = float(w) / h
164             # Panoramas get larger thumbnails because they look *awful*
165             if bigger_panoramas and w > 2 * h:
166                 max_size = max_size * 4
167             if w > h:
168                 w = max_size
169                 h = max_size / ratio
170             else:
171                 w = max_size * ratio
172                 h = max_size
173             w = int(w)
174             h = int(h)
175             tree.attrib.pop("width")
176             tree.attrib.pop("height")
177             tree.attrib['viewport'] = "0 0 %ipx %ipx" % (w, h)
178             if dst.endswith('.svgz'):
179                 op = gzip.GzipFile(dst, 'wb')
180             else:
181                 op = open(dst, 'wb')
182             op.write(lxml.etree.tostring(tree))
183             op.close()
184         except (KeyError, AttributeError) as e:
185             self.logger.warning("No width/height in %s. Original exception: %s" % (src, e))
186             utils.copy_file(src, dst)
187 
188     def image_date(self, src):
189         """Try to figure out the date of the image."""
190         if src not in self.dates:
191             try:
192                 im = Image.open(src)
193                 exif = im._getexif()
194             except Exception:
195                 exif = None
196             if exif is not None:
197                 for tag, value in list(exif.items()):
198                     decoded = ExifTags.TAGS.get(tag, tag)
199                     if decoded in ('DateTimeOriginal', 'DateTimeDigitized'):
200                         try:
201                             if isinstance(value, tuple):
202                                 value = value[0]
203                             self.dates[src] = datetime.datetime.strptime(
204                                 value, '%Y:%m:%d %H:%M:%S')
205                             break
206                         except ValueError:  # Invalid EXIF date.
207                             pass
208         if src not in self.dates:
209             self.dates[src] = datetime.datetime.fromtimestamp(
210                 os.stat(src).st_mtime)
211         return self.dates[src]
212
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 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

{"golden_diff": "diff --git a/nikola/image_processing.py b/nikola/image_processing.py\n--- a/nikola/image_processing.py\n+++ b/nikola/image_processing.py\n@@ -83,13 +83,15 @@\n \n     def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False):\n         \"\"\"Make a copy of the image in the requested size.\"\"\"\n-        if os.path.splitext(src)[1] in ['.svg', '.svgz']:\n+        extension = os.path.splitext(src)[1].lower()\n+        if extension in {'.svg', '.svgz'}:\n             self.resize_svg(src, dst, max_size, bigger_panoramas)\n             return\n \n         im = Image.open(src)\n \n-        if hasattr(im, 'n_frames') and im.n_frames > 1:\n+        # The jpg exclusion is Issue #3332\n+        if hasattr(im, 'n_frames') and im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}:\n             # Animated gif, leave as-is\n             utils.copy_file(src, dst)\n             return\n", "issue": "Thumbnail images are with the same size like the source images\n<!--\r\nBefore creating an issue:\r\n* make sure you are using an up-to-date version of Nikola\r\n* search for existing issues that might be related\r\n\r\nMake sure to:\r\n* provide information about your environment (below)\r\n* include all the output you get, and any other information related to your problem\r\n\r\nNikola v7.6.4, as provided by Ubuntu, is NOT SUPPORTED.\r\nIf you are using this version, you should upgrade: https://getnikola.com/getting-started.html\r\n-->\r\n\r\n### Environment\r\n\r\n**Python Version:**\r\nPython 3.7.3\r\n**Nikola Version:**\r\nNikola v8.0.3\r\n**Operating System:**\r\nopenSuse Tumbleweed / Windows 10\r\n### Description:\r\nAfter 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]. \r\n\r\n> -rw-r--r-- 1 marcus users 21771945 Dec 22 14:04 _63_1020.JPG\r\n-rw-r--r-- 1 marcus users 21771945 Dec 22 14:04 _63_1020.thumbnail.JPG\r\n-rw-r--r-- 1 marcus users 21936578 Dec 22 14:04 _63_1021.JPG\r\n-rw-r--r-- 1 marcus users 21936578 Dec 22 14:04 _63_1021.thumbnail.JPG\r\n-rw-r--r-- 1 marcus users 10862571 Dec 22 13:59 _DSC0849.JPG\r\n-rw-r--r-- 1 marcus users 10862571 Dec 22 13:59 _DSC0849.thumbnail.JPG\r\n-rw-r--r-- 1 marcus users  9256726 Dec 22 13:59 _DSC0850.JPG\r\n-rw-r--r-- 1 marcus users  9256726 Dec 22 13:59 _DSC0850.thumbnail.JPG\r\n-rw-r--r-- 1 marcus users 10139398 Dec 22 13:59 _DSC0851.JPG\r\n-rw-r--r-- 1 marcus users 10139398 Dec 22 13:59 _DSC0851.thumbnail.JPG\r\n-rw-r--r-- 1 marcus users 10533825 Dec 22 13:59 _DSC0852.JPG\r\n-rw-r--r-- 1 marcus users 10533825 Dec 22 13:59 _DSC0852.thumbnail.JPG\r\n-rw-r--r-- 1 marcus users   105627 Dec 22 14:03 IMG_0204.jpg\r\n-rw-r--r-- 1 marcus users     3781 Dec 22 14:03 IMG_0204.thumbnail.jpg\r\n-rw-r--r-- 1 marcus users   120466 Dec 22 14:03 IMG_0205.jpg\r\n-rw-r--r-- 1 marcus users     3541 Dec 22 14:03 IMG_0205.thumbnail.jpg\r\n-rw-r--r-- 1 marcus users   129014 Dec 22 14:03 IMG_0206.jpg\r\n-rw-r--r-- 1 marcus users     4713 Dec 22 14:03 IMG_0206.thumbnail.jpg\r\n-rw-r--r-- 1 marcus users   129206 Dec 22 14:03 IMG_0207.jpg\r\n-rw-r--r-- 1 marcus users     4826 Dec 22 14:03 IMG_0207.thumbnail.jpg\r\n-rw-r--r-- 1 marcus users     6481 Dec 22 14:04 index.html\r\n-rw-r--r-- 1 marcus users     4129 Dec 22 14:04 rss.xml\r\n\r\n[1] My personal photo blog https://gitlab.com/marcuskammer/photoblog/tree/nikola/photoblog/galleries/travel\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\n# Copyright \u00a9 2014 Roberto Alsina and others.\n\n# Permission is hereby granted, free of charge, to any\n# person obtaining a copy of this software and associated\n# documentation files (the \"Software\"), to deal in the\n# Software without restriction, including without limitation\n# the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the\n# Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice\n# shall be included in all copies or substantial portions of\n# the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY\n# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR\n# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS\n# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR\n# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\"\"\"Process images.\"\"\"\n\nimport datetime\nimport gzip\nimport os\nimport re\n\nimport lxml\nimport piexif\nfrom PIL import ExifTags, Image\n\nfrom nikola import utils\n\nEXIF_TAG_NAMES = {}\n\n\nclass ImageProcessor(object):\n    \"\"\"Apply image operations.\"\"\"\n\n    image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff']\n\n    def _fill_exif_tag_names(self):\n        \"\"\"Connect EXIF tag names to numeric values.\"\"\"\n        if not EXIF_TAG_NAMES:\n            for ifd in piexif.TAGS:\n                for tag, data in piexif.TAGS[ifd].items():\n                    EXIF_TAG_NAMES[tag] = data['name']\n\n    def filter_exif(self, exif, whitelist):\n        \"\"\"Filter EXIF data as described in the documentation.\"\"\"\n        # Scenario 1: keep everything\n        if whitelist == {'*': '*'}:\n            return exif\n\n        # Scenario 2: keep nothing\n        if whitelist == {}:\n            return None\n\n        # Scenario 3: keep some\n        self._fill_exif_tag_names()\n        exif = exif.copy()  # Don't modify in-place, it's rude\n        for k in list(exif.keys()):\n            if type(exif[k]) != dict:\n                pass  # At least thumbnails have no fields\n            elif k not in whitelist:\n                exif.pop(k)  # Not whitelisted, remove\n            elif k in whitelist and whitelist[k] == '*':\n                # Fully whitelisted, keep all\n                pass\n            else:\n                # Partially whitelisted\n                for tag in list(exif[k].keys()):\n                    if EXIF_TAG_NAMES[tag] not in whitelist[k]:\n                        exif[k].pop(tag)\n\n        return exif or None\n\n    def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False):\n        \"\"\"Make a copy of the image in the requested size.\"\"\"\n        if os.path.splitext(src)[1] in ['.svg', '.svgz']:\n            self.resize_svg(src, dst, max_size, bigger_panoramas)\n            return\n\n        im = Image.open(src)\n\n        if hasattr(im, 'n_frames') and im.n_frames > 1:\n            # Animated gif, leave as-is\n            utils.copy_file(src, dst)\n            return\n\n        size = w, h = im.size\n        if w > max_size or h > max_size:\n            size = max_size, max_size\n\n            # Panoramas get larger thumbnails because they look *awful*\n            if bigger_panoramas and w > 2 * h:\n                size = min(w, max_size * 4), min(w, max_size * 4)\n\n        try:\n            exif = piexif.load(im.info[\"exif\"])\n        except KeyError:\n            exif = None\n        # Inside this if, we can manipulate exif as much as\n        # we want/need and it will be preserved if required\n        if exif is not None:\n            # Rotate according to EXIF\n            value = exif['0th'].get(piexif.ImageIFD.Orientation, 1)\n            if value in (3, 4):\n                im = im.transpose(Image.ROTATE_180)\n            elif value in (5, 6):\n                im = im.transpose(Image.ROTATE_270)\n            elif value in (7, 8):\n                im = im.transpose(Image.ROTATE_90)\n            if value in (2, 4, 5, 7):\n                im = im.transpose(Image.FLIP_LEFT_RIGHT)\n            exif['0th'][piexif.ImageIFD.Orientation] = 1\n\n        try:\n            icc_profile = im.info.get('icc_profile') if preserve_icc_profiles else None\n            im.thumbnail(size, Image.ANTIALIAS)\n            if exif is not None and preserve_exif_data:\n                # Put right size in EXIF data\n                w, h = im.size\n                if '0th' in exif:\n                    exif[\"0th\"][piexif.ImageIFD.ImageWidth] = w\n                    exif[\"0th\"][piexif.ImageIFD.ImageLength] = h\n                if 'Exif' in exif:\n                    exif[\"Exif\"][piexif.ExifIFD.PixelXDimension] = w\n                    exif[\"Exif\"][piexif.ExifIFD.PixelYDimension] = h\n                # Filter EXIF data as required\n                exif = self.filter_exif(exif, exif_whitelist)\n                im.save(dst, exif=piexif.dump(exif), icc_profile=icc_profile)\n            else:\n                im.save(dst, icc_profile=icc_profile)\n        except Exception as e:\n            self.logger.warning(\"Can't process {0}, using original \"\n                                \"image! ({1})\".format(src, e))\n            utils.copy_file(src, dst)\n\n    def resize_svg(self, src, dst, max_size, bigger_panoramas):\n        \"\"\"Make a copy of an svg at the requested size.\"\"\"\n        try:\n            # Resize svg based on viewport hacking.\n            # note that this can also lead to enlarged svgs\n            if src.endswith('.svgz'):\n                with gzip.GzipFile(src, 'rb') as op:\n                    xml = op.read()\n            else:\n                with open(src, 'rb') as op:\n                    xml = op.read()\n            tree = lxml.etree.XML(xml)\n            width = tree.attrib['width']\n            height = tree.attrib['height']\n            w = int(re.search(\"[0-9]+\", width).group(0))\n            h = int(re.search(\"[0-9]+\", height).group(0))\n            # calculate new size preserving aspect ratio.\n            ratio = float(w) / h\n            # Panoramas get larger thumbnails because they look *awful*\n            if bigger_panoramas and w > 2 * h:\n                max_size = max_size * 4\n            if w > h:\n                w = max_size\n                h = max_size / ratio\n            else:\n                w = max_size * ratio\n                h = max_size\n            w = int(w)\n            h = int(h)\n            tree.attrib.pop(\"width\")\n            tree.attrib.pop(\"height\")\n            tree.attrib['viewport'] = \"0 0 %ipx %ipx\" % (w, h)\n            if dst.endswith('.svgz'):\n                op = gzip.GzipFile(dst, 'wb')\n            else:\n                op = open(dst, 'wb')\n            op.write(lxml.etree.tostring(tree))\n            op.close()\n        except (KeyError, AttributeError) as e:\n            self.logger.warning(\"No width/height in %s. Original exception: %s\" % (src, e))\n            utils.copy_file(src, dst)\n\n    def image_date(self, src):\n        \"\"\"Try to figure out the date of the image.\"\"\"\n        if src not in self.dates:\n            try:\n                im = Image.open(src)\n                exif = im._getexif()\n            except Exception:\n                exif = None\n            if exif is not None:\n                for tag, value in list(exif.items()):\n                    decoded = ExifTags.TAGS.get(tag, tag)\n                    if decoded in ('DateTimeOriginal', 'DateTimeDigitized'):\n                        try:\n                            if isinstance(value, tuple):\n                                value = value[0]\n                            self.dates[src] = datetime.datetime.strptime(\n                                value, '%Y:%m:%d %H:%M:%S')\n                            break\n                        except ValueError:  # Invalid EXIF date.\n                            pass\n        if src not in self.dates:\n            self.dates[src] = datetime.datetime.fromtimestamp(\n                os.stat(src).st_mtime)\n        return self.dates[src]\n", "path": "nikola/image_processing.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\n# Copyright \u00a9 2014 Roberto Alsina and others.\n\n# Permission is hereby granted, free of charge, to any\n# person obtaining a copy of this software and associated\n# documentation files (the \"Software\"), to deal in the\n# Software without restriction, including without limitation\n# the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the\n# Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice\n# shall be included in all copies or substantial portions of\n# the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY\n# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR\n# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS\n# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR\n# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\"\"\"Process images.\"\"\"\n\nimport datetime\nimport gzip\nimport os\nimport re\n\nimport lxml\nimport piexif\nfrom PIL import ExifTags, Image\n\nfrom nikola import utils\n\nEXIF_TAG_NAMES = {}\n\n\nclass ImageProcessor(object):\n    \"\"\"Apply image operations.\"\"\"\n\n    image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff']\n\n    def _fill_exif_tag_names(self):\n        \"\"\"Connect EXIF tag names to numeric values.\"\"\"\n        if not EXIF_TAG_NAMES:\n            for ifd in piexif.TAGS:\n                for tag, data in piexif.TAGS[ifd].items():\n                    EXIF_TAG_NAMES[tag] = data['name']\n\n    def filter_exif(self, exif, whitelist):\n        \"\"\"Filter EXIF data as described in the documentation.\"\"\"\n        # Scenario 1: keep everything\n        if whitelist == {'*': '*'}:\n            return exif\n\n        # Scenario 2: keep nothing\n        if whitelist == {}:\n            return None\n\n        # Scenario 3: keep some\n        self._fill_exif_tag_names()\n        exif = exif.copy()  # Don't modify in-place, it's rude\n        for k in list(exif.keys()):\n            if type(exif[k]) != dict:\n                pass  # At least thumbnails have no fields\n            elif k not in whitelist:\n                exif.pop(k)  # Not whitelisted, remove\n            elif k in whitelist and whitelist[k] == '*':\n                # Fully whitelisted, keep all\n                pass\n            else:\n                # Partially whitelisted\n                for tag in list(exif[k].keys()):\n                    if EXIF_TAG_NAMES[tag] not in whitelist[k]:\n                        exif[k].pop(tag)\n\n        return exif or None\n\n    def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False):\n        \"\"\"Make a copy of the image in the requested size.\"\"\"\n        extension = os.path.splitext(src)[1].lower()\n        if extension in {'.svg', '.svgz'}:\n            self.resize_svg(src, dst, max_size, bigger_panoramas)\n            return\n\n        im = Image.open(src)\n\n        # The jpg exclusion is Issue #3332\n        if hasattr(im, 'n_frames') and im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}:\n            # Animated gif, leave as-is\n            utils.copy_file(src, dst)\n            return\n\n        size = w, h = im.size\n        if w > max_size or h > max_size:\n            size = max_size, max_size\n\n            # Panoramas get larger thumbnails because they look *awful*\n            if bigger_panoramas and w > 2 * h:\n                size = min(w, max_size * 4), min(w, max_size * 4)\n\n        try:\n            exif = piexif.load(im.info[\"exif\"])\n        except KeyError:\n            exif = None\n        # Inside this if, we can manipulate exif as much as\n        # we want/need and it will be preserved if required\n        if exif is not None:\n            # Rotate according to EXIF\n            value = exif['0th'].get(piexif.ImageIFD.Orientation, 1)\n            if value in (3, 4):\n                im = im.transpose(Image.ROTATE_180)\n            elif value in (5, 6):\n                im = im.transpose(Image.ROTATE_270)\n            elif value in (7, 8):\n                im = im.transpose(Image.ROTATE_90)\n            if value in (2, 4, 5, 7):\n                im = im.transpose(Image.FLIP_LEFT_RIGHT)\n            exif['0th'][piexif.ImageIFD.Orientation] = 1\n\n        try:\n            icc_profile = im.info.get('icc_profile') if preserve_icc_profiles else None\n            im.thumbnail(size, Image.ANTIALIAS)\n            if exif is not None and preserve_exif_data:\n                # Put right size in EXIF data\n                w, h = im.size\n                if '0th' in exif:\n                    exif[\"0th\"][piexif.ImageIFD.ImageWidth] = w\n                    exif[\"0th\"][piexif.ImageIFD.ImageLength] = h\n                if 'Exif' in exif:\n                    exif[\"Exif\"][piexif.ExifIFD.PixelXDimension] = w\n                    exif[\"Exif\"][piexif.ExifIFD.PixelYDimension] = h\n                # Filter EXIF data as required\n                exif = self.filter_exif(exif, exif_whitelist)\n                im.save(dst, exif=piexif.dump(exif), icc_profile=icc_profile)\n            else:\n                im.save(dst, icc_profile=icc_profile)\n        except Exception as e:\n            self.logger.warning(\"Can't process {0}, using original \"\n                                \"image! ({1})\".format(src, e))\n            utils.copy_file(src, dst)\n\n    def resize_svg(self, src, dst, max_size, bigger_panoramas):\n        \"\"\"Make a copy of an svg at the requested size.\"\"\"\n        try:\n            # Resize svg based on viewport hacking.\n            # note that this can also lead to enlarged svgs\n            if src.endswith('.svgz'):\n                with gzip.GzipFile(src, 'rb') as op:\n                    xml = op.read()\n            else:\n                with open(src, 'rb') as op:\n                    xml = op.read()\n            tree = lxml.etree.XML(xml)\n            width = tree.attrib['width']\n            height = tree.attrib['height']\n            w = int(re.search(\"[0-9]+\", width).group(0))\n            h = int(re.search(\"[0-9]+\", height).group(0))\n            # calculate new size preserving aspect ratio.\n            ratio = float(w) / h\n            # Panoramas get larger thumbnails because they look *awful*\n            if bigger_panoramas and w > 2 * h:\n                max_size = max_size * 4\n            if w > h:\n                w = max_size\n                h = max_size / ratio\n            else:\n                w = max_size * ratio\n                h = max_size\n            w = int(w)\n            h = int(h)\n            tree.attrib.pop(\"width\")\n            tree.attrib.pop(\"height\")\n            tree.attrib['viewport'] = \"0 0 %ipx %ipx\" % (w, h)\n            if dst.endswith('.svgz'):\n                op = gzip.GzipFile(dst, 'wb')\n            else:\n                op = open(dst, 'wb')\n            op.write(lxml.etree.tostring(tree))\n            op.close()\n        except (KeyError, AttributeError) as e:\n            self.logger.warning(\"No width/height in %s. Original exception: %s\" % (src, e))\n            utils.copy_file(src, dst)\n\n    def image_date(self, src):\n        \"\"\"Try to figure out the date of the image.\"\"\"\n        if src not in self.dates:\n            try:\n                im = Image.open(src)\n                exif = im._getexif()\n            except Exception:\n                exif = None\n            if exif is not None:\n                for tag, value in list(exif.items()):\n                    decoded = ExifTags.TAGS.get(tag, tag)\n                    if decoded in ('DateTimeOriginal', 'DateTimeDigitized'):\n                        try:\n                            if isinstance(value, tuple):\n                                value = value[0]\n                            self.dates[src] = datetime.datetime.strptime(\n                                value, '%Y:%m:%d %H:%M:%S')\n                            break\n                        except ValueError:  # Invalid EXIF date.\n                            pass\n        if src not in self.dates:\n            self.dates[src] = datetime.datetime.fromtimestamp(\n                os.stat(src).st_mtime)\n        return self.dates[src]\n", "path": "nikola/image_processing.py"}]}