We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
How could Nikola be able to process WebP format image?
I would like to use webp picture in whole site, but when I built, it reported as:

```
.  scale_images:output\images\gm-blur-shown-grid.webp
[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\gm-blur-shown-grid.webp, using original image! (a bytes-like object is required, not 'NoneType')
[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\gm-blur-shown-grid.webp, using original image! (a bytes-like object is required, not 'NoneType')
.  scale_images:output\images\gm-blur-shown.webp
[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\gm-blur-shown.webp, using original image! (a bytes-like object is required, not 'NoneType')
[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\gm-blur-shown.webp, using original image! (a bytes-like object is required, not 'NoneType')
```

There were raw webp pictures in my `output` directory, but there won't generate thumbnails.

And then, I search in conf.py, Google and GitHub issue, but didn't find any description to enable this feature.
Is there any way to use webp in nikola site?
How could Nikola be able to process WebP format image?
I would like to use webp picture in whole site, but when I built, it reported as:

```
.  scale_images:output\images\gm-blur-shown-grid.webp
[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\gm-blur-shown-grid.webp, using original image! (a bytes-like object is required, not 'NoneType')
[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\gm-blur-shown-grid.webp, using original image! (a bytes-like object is required, not 'NoneType')
.  scale_images:output\images\gm-blur-shown.webp
[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\gm-blur-shown.webp, using original image! (a bytes-like object is required, not 'NoneType')
[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\gm-blur-shown.webp, using original image! (a bytes-like object is required, not 'NoneType')
```

There were raw webp pictures in my `output` directory, but there won't generate thumbnails.

And then, I search in conf.py, Google and GitHub issue, but didn't find any description to enable this feature.
Is there any way to use webp in nikola site?

--- 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=None, max_size=None, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False, dst_paths=None, max_sizes=None):
85         """Make a copy of the image in the requested size(s).
86 
87         max_sizes should be a list of sizes, and the image would be resized to fit in a
88         square of each size (preserving aspect ratio).
89 
90         dst_paths is a list of the destination paths, and should be the same length as max_sizes.
91 
92         Backwards compatibility:
93 
94         * If max_sizes is None, it's set to [max_size]
95         * If dst_paths is None, it's set to [dst]
96         * Either max_size or max_sizes should be set
97         * Either dst or dst_paths should be set
98         """
99         if dst_paths is None:
100             dst_paths = [dst]
101         if max_sizes is None:
102             max_sizes = [max_size]
103         if len(max_sizes) != len(dst_paths):
104             raise ValueError('resize_image called with incompatible arguments: {} / {}'.format(dst_paths, max_sizes))
105         extension = os.path.splitext(src)[1].lower()
106         if extension in {'.svg', '.svgz'}:
107             self.resize_svg(src, dst_paths, max_sizes, bigger_panoramas)
108             return
109 
110         _im = Image.open(src)
111 
112         # The jpg exclusion is Issue #3332
113         is_animated = hasattr(_im, 'n_frames') and _im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}
114 
115         exif = None
116         if "exif" in _im.info:
117             exif = piexif.load(_im.info["exif"])
118             # Rotate according to EXIF
119             if "0th" in exif:
120                 value = exif['0th'].get(piexif.ImageIFD.Orientation, 1)
121                 if value in (3, 4):
122                     _im = _im.transpose(Image.ROTATE_180)
123                 elif value in (5, 6):
124                     _im = _im.transpose(Image.ROTATE_270)
125                 elif value in (7, 8):
126                     _im = _im.transpose(Image.ROTATE_90)
127                 if value in (2, 4, 5, 7):
128                     _im = _im.transpose(Image.FLIP_LEFT_RIGHT)
129                 exif['0th'][piexif.ImageIFD.Orientation] = 1
130             exif = self.filter_exif(exif, exif_whitelist)
131 
132         icc_profile = _im.info.get('icc_profile') if preserve_icc_profiles else None
133 
134         for dst, max_size in zip(dst_paths, max_sizes):
135             if is_animated:  # Animated gif, leave as-is
136                 utils.copy_file(src, dst)
137                 continue
138 
139             im = _im.copy()
140 
141             size = w, h = im.size
142             if w > max_size or h > max_size:
143                 size = max_size, max_size
144                 # Panoramas get larger thumbnails because they look *awful*
145                 if bigger_panoramas and w > 2 * h:
146                     size = min(w, max_size * 4), min(w, max_size * 4)
147             try:
148                 im.thumbnail(size, Image.ANTIALIAS)
149                 if exif is not None and preserve_exif_data:
150                     # Put right size in EXIF data
151                     w, h = im.size
152                     if '0th' in exif:
153                         exif["0th"][piexif.ImageIFD.ImageWidth] = w
154                         exif["0th"][piexif.ImageIFD.ImageLength] = h
155                     if 'Exif' in exif:
156                         exif["Exif"][piexif.ExifIFD.PixelXDimension] = w
157                         exif["Exif"][piexif.ExifIFD.PixelYDimension] = h
158                     # Filter EXIF data as required
159                     im.save(dst, exif=piexif.dump(exif), icc_profile=icc_profile)
160                 else:
161                     im.save(dst, icc_profile=icc_profile)
162             except Exception as e:
163                 self.logger.warning("Can't process {0}, using original "
164                                     "image! ({1})".format(src, e))
165                 utils.copy_file(src, dst)
166 
167     def resize_svg(self, src, dst_paths, max_sizes, bigger_panoramas):
168         """Make a copy of an svg at the requested sizes."""
169         # Resize svg based on viewport hacking.
170         # note that this can also lead to enlarged svgs
171         if src.endswith('.svgz'):
172             with gzip.GzipFile(src, 'rb') as op:
173                 xml = op.read()
174         else:
175             with open(src, 'rb') as op:
176                 xml = op.read()
177 
178         for dst, max_size in zip(dst_paths, max_sizes):
179             try:
180                 tree = lxml.etree.XML(xml)
181                 width = tree.attrib['width']
182                 height = tree.attrib['height']
183                 w = int(re.search("[0-9]+", width).group(0))
184                 h = int(re.search("[0-9]+", height).group(0))
185                 # calculate new size preserving aspect ratio.
186                 ratio = float(w) / h
187                 # Panoramas get larger thumbnails because they look *awful*
188                 if bigger_panoramas and w > 2 * h:
189                     max_size = max_size * 4
190                 if w > h:
191                     w = max_size
192                     h = max_size / ratio
193                 else:
194                     w = max_size * ratio
195                     h = max_size
196                 w = int(w)
197                 h = int(h)
198                 tree.attrib.pop("width")
199                 tree.attrib.pop("height")
200                 tree.attrib['viewport'] = "0 0 %ipx %ipx" % (w, h)
201                 if dst.endswith('.svgz'):
202                     op = gzip.GzipFile(dst, 'wb')
203                 else:
204                     op = open(dst, 'wb')
205                 op.write(lxml.etree.tostring(tree))
206                 op.close()
207             except (KeyError, AttributeError) as e:
208                 self.logger.warning("No width/height in %s. Original exception: %s" % (src, e))
209                 utils.copy_file(src, dst)
210 
211     def image_date(self, src):
212         """Try to figure out the date of the image."""
213         if src not in self.dates:
214             try:
215                 im = Image.open(src)
216                 exif = im._getexif()
217             except Exception:
218                 exif = None
219             if exif is not None:
220                 for tag, value in list(exif.items()):
221                     decoded = ExifTags.TAGS.get(tag, tag)
222                     if decoded in ('DateTimeOriginal', 'DateTimeDigitized'):
223                         try:
224                             if isinstance(value, tuple):
225                                 value = value[0]
226                             self.dates[src] = datetime.datetime.strptime(
227                                 value, '%Y:%m:%d %H:%M:%S')
228                             break
229                         except ValueError:  # Invalid EXIF date.
230                             pass
231         if src not in self.dates:
232             self.dates[src] = datetime.datetime.fromtimestamp(
233                 os.stat(src).st_mtime)
234         return self.dates[src]
235
```
--- 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
@@ -43,7 +43,7 @@
 class ImageProcessor(object):
     """Apply image operations."""
 
-    image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff']
+    image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff', '.webp']
 
     def _fill_exif_tag_names(self):
         """Connect EXIF tag names to numeric values."""
@@ -146,6 +146,10 @@
                     size = min(w, max_size * 4), min(w, max_size * 4)
             try:
                 im.thumbnail(size, Image.ANTIALIAS)
+                save_args = {}
+                if icc_profile:
+                    save_args['icc_profile'] = icc_profile
+
                 if exif is not None and preserve_exif_data:
                     # Put right size in EXIF data
                     w, h = im.size
@@ -156,9 +160,9 @@
                         exif["Exif"][piexif.ExifIFD.PixelXDimension] = w
                         exif["Exif"][piexif.ExifIFD.PixelYDimension] = h
                     # Filter EXIF data as required
-                    im.save(dst, exif=piexif.dump(exif), icc_profile=icc_profile)
-                else:
-                    im.save(dst, icc_profile=icc_profile)
+                    save_args['exif'] = piexif.dump(exif)
+
+                im.save(dst, **save_args)
             except Exception as e:
                 self.logger.warning("Can't process {0}, using original "
                                     "image! ({1})".format(src, e))

{"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@@ -43,7 +43,7 @@\n class ImageProcessor(object):\n     \"\"\"Apply image operations.\"\"\"\n \n-    image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff']\n+    image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff', '.webp']\n \n     def _fill_exif_tag_names(self):\n         \"\"\"Connect EXIF tag names to numeric values.\"\"\"\n@@ -146,6 +146,10 @@\n                     size = min(w, max_size * 4), min(w, max_size * 4)\n             try:\n                 im.thumbnail(size, Image.ANTIALIAS)\n+                save_args = {}\n+                if icc_profile:\n+                    save_args['icc_profile'] = icc_profile\n+\n                 if exif is not None and preserve_exif_data:\n                     # Put right size in EXIF data\n                     w, h = im.size\n@@ -156,9 +160,9 @@\n                         exif[\"Exif\"][piexif.ExifIFD.PixelXDimension] = w\n                         exif[\"Exif\"][piexif.ExifIFD.PixelYDimension] = h\n                     # Filter EXIF data as required\n-                    im.save(dst, exif=piexif.dump(exif), icc_profile=icc_profile)\n-                else:\n-                    im.save(dst, icc_profile=icc_profile)\n+                    save_args['exif'] = piexif.dump(exif)\n+\n+                im.save(dst, **save_args)\n             except Exception as e:\n                 self.logger.warning(\"Can't process {0}, using original \"\n                                     \"image! ({1})\".format(src, e))\n", "issue": "How could Nikola be able to process WebP format image?\nI would like to use webp picture in whole site, but when I built, it reported as:\r\n\r\n```\r\n.  scale_images:output\\images\\gm-blur-shown-grid.webp\r\n[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\\gm-blur-shown-grid.webp, using original image! (a bytes-like object is required, not 'NoneType')\r\n[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\\gm-blur-shown-grid.webp, using original image! (a bytes-like object is required, not 'NoneType')\r\n.  scale_images:output\\images\\gm-blur-shown.webp\r\n[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\\gm-blur-shown.webp, using original image! (a bytes-like object is required, not 'NoneType')\r\n[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\\gm-blur-shown.webp, using original image! (a bytes-like object is required, not 'NoneType')\r\n```\r\n\r\nThere were raw webp pictures in my `output` directory, but there won't generate thumbnails.\r\n\r\nAnd then, I search in conf.py, Google and GitHub issue, but didn't find any description to enable this feature.\r\nIs there any way to use webp in nikola site?\nHow could Nikola be able to process WebP format image?\nI would like to use webp picture in whole site, but when I built, it reported as:\r\n\r\n```\r\n.  scale_images:output\\images\\gm-blur-shown-grid.webp\r\n[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\\gm-blur-shown-grid.webp, using original image! (a bytes-like object is required, not 'NoneType')\r\n[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\\gm-blur-shown-grid.webp, using original image! (a bytes-like object is required, not 'NoneType')\r\n.  scale_images:output\\images\\gm-blur-shown.webp\r\n[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\\gm-blur-shown.webp, using original image! (a bytes-like object is required, not 'NoneType')\r\n[2020-04-27T18:28:19Z] WARNING: scale_images: Can't process images\\gm-blur-shown.webp, using original image! (a bytes-like object is required, not 'NoneType')\r\n```\r\n\r\nThere were raw webp pictures in my `output` directory, but there won't generate thumbnails.\r\n\r\nAnd then, I search in conf.py, Google and GitHub issue, but didn't find any description to enable this feature.\r\nIs there any way to use webp in nikola site?\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=None, max_size=None, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False, dst_paths=None, max_sizes=None):\n        \"\"\"Make a copy of the image in the requested size(s).\n\n        max_sizes should be a list of sizes, and the image would be resized to fit in a\n        square of each size (preserving aspect ratio).\n\n        dst_paths is a list of the destination paths, and should be the same length as max_sizes.\n\n        Backwards compatibility:\n\n        * If max_sizes is None, it's set to [max_size]\n        * If dst_paths is None, it's set to [dst]\n        * Either max_size or max_sizes should be set\n        * Either dst or dst_paths should be set\n        \"\"\"\n        if dst_paths is None:\n            dst_paths = [dst]\n        if max_sizes is None:\n            max_sizes = [max_size]\n        if len(max_sizes) != len(dst_paths):\n            raise ValueError('resize_image called with incompatible arguments: {} / {}'.format(dst_paths, max_sizes))\n        extension = os.path.splitext(src)[1].lower()\n        if extension in {'.svg', '.svgz'}:\n            self.resize_svg(src, dst_paths, max_sizes, bigger_panoramas)\n            return\n\n        _im = Image.open(src)\n\n        # The jpg exclusion is Issue #3332\n        is_animated = hasattr(_im, 'n_frames') and _im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}\n\n        exif = None\n        if \"exif\" in _im.info:\n            exif = piexif.load(_im.info[\"exif\"])\n            # Rotate according to EXIF\n            if \"0th\" in 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            exif = self.filter_exif(exif, exif_whitelist)\n\n        icc_profile = _im.info.get('icc_profile') if preserve_icc_profiles else None\n\n        for dst, max_size in zip(dst_paths, max_sizes):\n            if is_animated:  # Animated gif, leave as-is\n                utils.copy_file(src, dst)\n                continue\n\n            im = _im.copy()\n\n            size = w, h = im.size\n            if w > max_size or h > max_size:\n                size = max_size, max_size\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            try:\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                    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_paths, max_sizes, bigger_panoramas):\n        \"\"\"Make a copy of an svg at the requested sizes.\"\"\"\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\n        for dst, max_size in zip(dst_paths, max_sizes):\n            try:\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', '.webp']\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=None, max_size=None, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False, dst_paths=None, max_sizes=None):\n        \"\"\"Make a copy of the image in the requested size(s).\n\n        max_sizes should be a list of sizes, and the image would be resized to fit in a\n        square of each size (preserving aspect ratio).\n\n        dst_paths is a list of the destination paths, and should be the same length as max_sizes.\n\n        Backwards compatibility:\n\n        * If max_sizes is None, it's set to [max_size]\n        * If dst_paths is None, it's set to [dst]\n        * Either max_size or max_sizes should be set\n        * Either dst or dst_paths should be set\n        \"\"\"\n        if dst_paths is None:\n            dst_paths = [dst]\n        if max_sizes is None:\n            max_sizes = [max_size]\n        if len(max_sizes) != len(dst_paths):\n            raise ValueError('resize_image called with incompatible arguments: {} / {}'.format(dst_paths, max_sizes))\n        extension = os.path.splitext(src)[1].lower()\n        if extension in {'.svg', '.svgz'}:\n            self.resize_svg(src, dst_paths, max_sizes, bigger_panoramas)\n            return\n\n        _im = Image.open(src)\n\n        # The jpg exclusion is Issue #3332\n        is_animated = hasattr(_im, 'n_frames') and _im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}\n\n        exif = None\n        if \"exif\" in _im.info:\n            exif = piexif.load(_im.info[\"exif\"])\n            # Rotate according to EXIF\n            if \"0th\" in 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            exif = self.filter_exif(exif, exif_whitelist)\n\n        icc_profile = _im.info.get('icc_profile') if preserve_icc_profiles else None\n\n        for dst, max_size in zip(dst_paths, max_sizes):\n            if is_animated:  # Animated gif, leave as-is\n                utils.copy_file(src, dst)\n                continue\n\n            im = _im.copy()\n\n            size = w, h = im.size\n            if w > max_size or h > max_size:\n                size = max_size, max_size\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            try:\n                im.thumbnail(size, Image.ANTIALIAS)\n                save_args = {}\n                if icc_profile:\n                    save_args['icc_profile'] = icc_profile\n\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                    save_args['exif'] = piexif.dump(exif)\n\n                im.save(dst, **save_args)\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_paths, max_sizes, bigger_panoramas):\n        \"\"\"Make a copy of an svg at the requested sizes.\"\"\"\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\n        for dst, max_size in zip(dst_paths, max_sizes):\n            try:\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"}]}