Strip exif information from loaded image in Resizer#loadFile
authorTim Düsterhus <duesterhus@woltlab.com>
Thu, 2 Jul 2020 08:11:59 +0000 (10:11 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Thu, 2 Jul 2020 09:35:57 +0000 (11:35 +0200)
Modern browsers take the Exif orientation into account when showing a JPEG
within an HTMLImageElement. Unfortunately this orientation is not only
visual, but extends to the blob received when reading this image into a
canvas.

The JavaScript based image resizer using within the attachment system takes
care reinsert the original Exif data after fetching the resized blob from
pica.js.

This causes the image to be reoriented multiple times, ultimately leading
to an incorrectly oriented image:

1. The browser rotates the image.
2. The server rotates the image again, because the original Exif information
   has been preserved.

To fix this issue we strip the Exif information before handing the blob over
to the HTMLImageElement, forcing the browser to use the raw pixels instead
of pretending to be smart. When the Exif information is reinserted after
resizing the image that will be uploaded will then be reoriented only once:
On the server.

During fixing of this bug it was also investigated whether one can find out
whether the browser reoriented the image, it looks like one cannot. It was
also tested whether setting `image-orientation: none` will have any effect.

It only has in Firefox: When image-orientation: none is set you will get
the behavior as if no Exif information is present.
In Chrome the source image will not be be reoriented when rendered inside
of the DOM. Reading the pixel values however still returns the reoriented
garbage.

Thus stripping the exif information is the best solution to combat web
browsers attempting to be smart. Unfortunately it comes with an increased
processing requirement, because the raw blob (possible multiple megabytes)
will need to be processed to strip the Exif data.

wcfsetup/install/files/js/WoltLabSuite/Core/Image/Resizer.js

index 8d893dad5cb37e09359568ec6980a6b8d6767aa6..53687ba9022c935b2c3129232095bf3c79b795e5 100644 (file)
@@ -113,34 +113,41 @@ define([
                 */
                loadFile: function (file) {
                        var exif = undefined;
+                       var fileData = Promise.resolve(file);
                        if (file.type === 'image/jpeg') {
                                // Extract EXIF data
                                exif = ExifUtil.getExifBytesFromJpeg(file);
+                               
+                               // Strip EXIF data
+                               fileData = fileData.then(ExifUtil.removeExifData.bind(ExifUtil));
                        }
                        
-                       var loader = new Promise(function (resolve, reject) {
-                               var reader = new FileReader();
-                               var image = new Image();
-                               
-                               reader.addEventListener('load', function () {
-                                       image.src = reader.result;
-                               });
-                               
-                               reader.addEventListener('error', function () {
-                                       reader.abort();
-                                       reject(reader.error);
-                               });
-                               
-                               image.addEventListener('error', reject);
-                               
-                               image.addEventListener('load', function () {
-                                       resolve(image);
+                       var fileData = fileData
+                               .then(function (blob) {
+                                       return new Promise(function (resolve, reject) {
+                                               var reader = new FileReader();
+                                               var image = new Image();
+                                               
+                                               reader.addEventListener('load', function () {
+                                                       image.src = reader.result;
+                                               });
+                                               
+                                               reader.addEventListener('error', function () {
+                                                       reader.abort();
+                                                       reject(reader.error);
+                                               });
+                                               
+                                               image.addEventListener('error', reject);
+                                               
+                                               image.addEventListener('load', function () {
+                                                       resolve(image);
+                                               });
+                                               
+                                               reader.readAsDataURL(blob);
+                                       });
                                });
-                               
-                               reader.readAsDataURL(file);
-                       });
                        
-                       return Promise.all([ exif, loader ])
+                       return Promise.all([ exif, fileData ])
                                .then(function (result) {
                                        return { exif: result[0], image: result[1] };
                                });