From d95aeaf621df07a210236347b5f1a6516a2b7bb5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sun, 3 Mar 2019 18:00:30 +0100
Subject: [PATCH] misc: add option to resize images before upload (#23152)

---
 wcs/fields.py                                 |  15 +-
 wcs/qommon/form.py                            |   1 +
 wcs/qommon/static/js/qommon.fileupload.js     | 265 +++++++++++++++++-
 .../templates/qommon/forms/widgets/file.html  |   4 +-
 4 files changed, 274 insertions(+), 11 deletions(-)

diff --git a/wcs/fields.py b/wcs/fields.py
index f4d512b07..7640f88e1 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -858,10 +858,16 @@ class FileField(WidgetField):
     description = N_('File Upload')
     document_type = None
     max_file_size = None
+    automatic_image_resize = True
     allow_portfolio_picking = False
 
     widget_class = FileWithPreviewWidget
-    extra_attributes = ['file_type', 'max_file_size', 'allow_portfolio_picking']
+    extra_attributes = [
+            'file_type',
+            'max_file_size',
+            'allow_portfolio_picking',
+            'automatic_image_resize',
+            ]
 
     def __init__(self, *args, **kwargs):
         super(FileField, self).__init__(*args, **kwargs)
@@ -890,6 +896,10 @@ class FileField(WidgetField):
         form.add(FileSizeWidget, 'max_file_size', title=_('Max file size'),
                 value=self.max_file_size,
                 advanced=not(self.max_file_size))
+        form.add(CheckboxWidget, 'automatic_image_resize',
+                title=_('Automatically resize uploaded images'),
+                value=self.automatic_image_resize,
+                advanced=True)
         if portfolio.has_portfolio():
             form.add(CheckboxWidget, 'allow_portfolio_picking',
                     title=_('Allow user to pick a file from a portfolio'),
@@ -898,7 +908,8 @@ class FileField(WidgetField):
 
     def get_admin_attributes(self):
         return WidgetField.get_admin_attributes(self) + [
-                'document_type', 'max_file_size', 'allow_portfolio_picking']
+                'document_type', 'max_file_size', 'allow_portfolio_picking',
+                'automatic_image_resize']
 
     @classmethod
     def convert_value_from_anything(cls, value):
diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py
index 8fd381952..077185f48 100644
--- a/wcs/qommon/form.py
+++ b/wcs/qommon/form.py
@@ -655,6 +655,7 @@ class FileWithPreviewWidget(CompositeWidget):
         self.value = value
         self.readonly = kwargs.get('readonly')
         self.max_file_size = kwargs.pop('max_file_size', None)
+        self.automatic_image_resize = kwargs.pop('automatic_image_resize', False)
         self.allow_portfolio_picking = has_portfolio() and kwargs.pop('allow_portfolio_picking', True)
         if self.max_file_size:
             self.max_file_size_bytes = FileSizeWidget.parse_file_size(self.max_file_size)
diff --git a/wcs/qommon/static/js/qommon.fileupload.js b/wcs/qommon/static/js/qommon.fileupload.js
index 13868e25f..8daca73e6 100644
--- a/wcs/qommon/static/js/qommon.fileupload.js
+++ b/wcs/qommon/static/js/qommon.fileupload.js
@@ -1,6 +1,201 @@
+var ExifRestorer = (function()
+{
+    // from http://www.perry.cz/files/ExifRestorer.js
+    // based on MinifyJpeg
+    // http://elicon.blog57.fc2.com/blog-entry-206.html
+
+    var ExifRestorer = {};
+
+    ExifRestorer.KEY_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+
+    ExifRestorer.encode64 = function(input)
+    {
+        var output = "",
+            chr1, chr2, chr3 = "",
+            enc1, enc2, enc3, enc4 = "",
+            i = 0;
+
+        do {
+            chr1 = input[i++];
+            chr2 = input[i++];
+            chr3 = input[i++];
+
+            enc1 = chr1 >> 2;
+            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+            enc4 = chr3 & 63;
+
+            if (isNaN(chr2)) {
+               enc3 = enc4 = 64;
+            } else if (isNaN(chr3)) {
+               enc4 = 64;
+            }
+
+            output = output +
+               this.KEY_STR.charAt(enc1) +
+               this.KEY_STR.charAt(enc2) +
+               this.KEY_STR.charAt(enc3) +
+               this.KEY_STR.charAt(enc4);
+            chr1 = chr2 = chr3 = "";
+            enc1 = enc2 = enc3 = enc4 = "";
+        } while (i < input.length);
+
+        return output;
+    };
+
+    ExifRestorer.restore = function(origFileBase64, resizedFileBase64)
+    {
+        if (!origFileBase64.match("data:image/jpeg;base64,"))
+        {
+            return resizedFileBase64;
+        }
+
+        var rawImage = this.decode64(origFileBase64.replace("data:image/jpeg;base64,", ""));
+        var segments = this.slice2Segments(rawImage);
+
+        var image = this.exifManipulation(resizedFileBase64, segments);
+
+        return this.encode64(image);
+    };
+
+    ExifRestorer.restore_as_blob = function(origFileBase64, resizedFileBase64) {
+        var b64Data = ExifRestorer.restore(origFileBase64, resizedFileBase64);
+        contentType = 'image/jpeg';
+        sliceSize = 512;
+
+        var byteCharacters = atob(b64Data);
+        var byteArrays = [];
+
+        for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
+            var slice = byteCharacters.slice(offset, offset + sliceSize);
+            var byteNumbers = new Array(slice.length);
+            for (var i = 0; i < slice.length; i++) {
+                byteNumbers[i] = slice.charCodeAt(i);
+            }
+            var byteArray = new Uint8Array(byteNumbers);
+            byteArrays.push(byteArray);
+        }
+        return new Blob(byteArrays, {type: contentType});
+    };
+
+    ExifRestorer.exifManipulation = function(resizedFileBase64, segments)
+    {
+        var exifArray = this.getExifArray(segments),
+            newImageArray = this.insertExif(resizedFileBase64, exifArray),
+            aBuffer = new Uint8Array(newImageArray);
+
+        return aBuffer;
+    };
+
+    ExifRestorer.getExifArray = function(segments)
+    {
+        var seg;
+        for (var x = 0; x < segments.length; x++)
+        {
+            seg = segments[x];
+            if (seg[0] == 255 & seg[1] == 225) //(ff e1)
+            {
+                return seg;
+            }
+        }
+        return [];
+    };
+
+    ExifRestorer.insertExif = function(resizedFileBase64, exifArray)
+    {
+        var imageData = resizedFileBase64.replace("data:image/jpeg;base64,", ""),
+            buf = this.decode64(imageData),
+            separatePoint = buf.indexOf(255,3),
+            mae = buf.slice(0, separatePoint),
+            ato = buf.slice(separatePoint),
+            array = mae;
+
+        array = array.concat(exifArray);
+        array = array.concat(ato);
+        return array;
+    };
+
+
+    ExifRestorer.slice2Segments = function(rawImageArray)
+    {
+        var head = 0,
+            segments = [];
+
+        while (1)
+        {
+            if (rawImageArray[head] == 255 & rawImageArray[head + 1] == 218){break;}
+            if (rawImageArray[head] == 255 & rawImageArray[head + 1] == 216)
+            {
+                head += 2;
+            }
+            else
+            {
+                var length = rawImageArray[head + 2] * 256 + rawImageArray[head + 3],
+                    endPoint = head + length + 2,
+                    seg = rawImageArray.slice(head, endPoint);
+                segments.push(seg);
+                head = endPoint;
+            }
+            if (head > rawImageArray.length){break;}
+        }
+
+        return segments;
+    };
+
+    ExifRestorer.decode64 = function(input)
+    {
+        var output = "",
+            chr1, chr2, chr3 = "",
+            enc1, enc2, enc3, enc4 = "",
+            i = 0,
+            buf = [];
+
+        // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
+        var base64test = /[^A-Za-z0-9\+\/\=]/g;
+        if (base64test.exec(input)) {
+            alert("There were invalid base64 characters in the input text.\n" +
+                  "Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" +
+                  "Expect errors in decoding.");
+        }
+        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
+
+        do {
+            enc1 = this.KEY_STR.indexOf(input.charAt(i++));
+            enc2 = this.KEY_STR.indexOf(input.charAt(i++));
+            enc3 = this.KEY_STR.indexOf(input.charAt(i++));
+            enc4 = this.KEY_STR.indexOf(input.charAt(i++));
+
+            chr1 = (enc1 << 2) | (enc2 >> 4);
+            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+            chr3 = ((enc3 & 3) << 6) | enc4;
+
+            buf.push(chr1);
+
+            if (enc3 != 64) {
+               buf.push(chr2);
+            }
+            if (enc4 != 64) {
+               buf.push(chr3);
+            }
+
+            chr1 = chr2 = chr3 = "";
+            enc1 = enc2 = enc3 = enc4 = "";
+
+        } while (i < input.length);
+
+        return buf;
+    };
+
+    return ExifRestorer;
+})();
+
 $.WcsFileUpload = {
     prepare: function() {
         var base_widget = $(this);
+        var image_resize = $(this).find('.file-button').data('image-resize');
+        if (typeof(FileReader) === "undefined") {
+            image_resize = false;
+        }
         if ($(base_widget).find('input[type=hidden]').val()) {
             $(base_widget).find('input[type=file]').hide();
             $(base_widget).find('.use-file-from-fargo').hide();
@@ -14,14 +209,56 @@ $.WcsFileUpload = {
             pasteZone: base_widget,
             dataType: 'json',
             add: function (e, data) {
-                $(base_widget).find('.fileprogress').removeClass('upload-error');
-                $(base_widget).find('.fileprogress .bar').text(
-                        $(base_widget).find('.fileprogress .bar').data('upload'));
-                $(base_widget).find('.fileprogress .bar').css('width', '0%');
-                $(base_widget).find('.fileprogress').show();
-                $(base_widget).find('.fileinfo').hide();
-                $(base_widget).parents('form').find('input[name=submit]').prop('disabled', true);
-                var jqXHR = data.submit();
+                if (image_resize && (
+                        data.files[0].type == 'image/jpeg' ||
+                        data.files[0].type == 'image/png')) {
+
+                    $(base_widget).find('.fileprogress .bar').css('width', '100%');
+                    $(base_widget).find('.fileprogress .bar').text(
+                        $(base_widget).find('.fileprogress .bar').data('resize'));
+                    $(base_widget).find('.fileprogress').show();
+
+                    var reader = new FileReader();
+                    reader.onload = function(e) {
+                        var original_image_64 = e.target.result;
+                        var img = document.createElement("img");
+                        img.onload = function() {
+                            var canvas = document.createElement("canvas");
+                            var ctx = canvas.getContext('2d');
+                            ctx.drawImage(img, 0, 0);
+
+                            var MAX_WIDTH = 2000;
+                            var MAX_HEIGHT = 2000;
+                            var width = img.width;
+                            var height = img.height;
+
+                            if (width > height) {
+                                if (width > MAX_WIDTH) {
+                                    height *= MAX_WIDTH / width;
+                                    width = MAX_WIDTH;
+                                }
+                            } else {
+                                if (height > MAX_HEIGHT) {
+                                    width *= MAX_HEIGHT / height;
+                                    height = MAX_HEIGHT;
+                                }
+                            }
+                            canvas.width = width;
+                            canvas.height = height;
+                            var ctx = canvas.getContext('2d');
+                            ctx.drawImage(img, 0, 0, width, height);
+                            var new_image_64 = canvas.toDataURL('image/jpeg', 0.95);
+                            var blob = ExifRestorer.restore_as_blob(original_image_64, new_image_64);
+                            blob.name = data.files[0].name;
+                            data.files[0] = blob;
+                            return $.WcsFileUpload.upload(base_widget, data);
+                        }
+                        img.src = e.target.result;
+                    }
+                    reader.readAsDataURL(data.files[0]);
+                } else {
+                  return $.WcsFileUpload.upload(base_widget, data);
+                }
             },
             done: function(e, data) {
                 $(base_widget).find('.fileprogress').hide();
@@ -57,7 +294,19 @@ $.WcsFileUpload = {
             $(base_widget).find('input[type=file]').click();
             return false;
         });
+    },
+
+    upload: function(base_widget, data) {
+        $(base_widget).find('.fileprogress').removeClass('upload-error');
+        $(base_widget).find('.fileprogress .bar').text(
+                $(base_widget).find('.fileprogress .bar').data('upload'));
+        $(base_widget).find('.fileprogress .bar').css('width', '0%');
+        $(base_widget).find('.fileprogress').show();
+        $(base_widget).find('.fileinfo').hide();
+        $(base_widget).parents('form').find('input[name=submit]').prop('disabled', true);
+        var jqXHR = data.submit();
     }
+
 }
 
 $(function() {
diff --git a/wcs/qommon/templates/qommon/forms/widgets/file.html b/wcs/qommon/templates/qommon/forms/widgets/file.html
index 227ff22f0..bb24204aa 100644
--- a/wcs/qommon/templates/qommon/forms/widgets/file.html
+++ b/wcs/qommon/templates/qommon/forms/widgets/file.html
@@ -2,7 +2,8 @@
 {% load i18n %}
 
 {% block widget-control %}
-<div class="file-button {% if widget.is_image %}file-image{% endif %}">
+<div class="file-button {% if widget.is_image %}file-image{% endif %}"
+     {% if widget.automatic_image_resize %}data-image-resize="true"{% endif %}>
 {% for w in widget.get_widgets %}
   {{ w.render|safe }}
 {% endfor %}
@@ -16,6 +17,7 @@
 <div class="fileprogress" style="display: none;">
   <div class="bar"
        data-upload="{% trans "Upload in progress..." %}"
+       data-resize="{% trans "Resizing image..." %}"
        data-error="{% trans "Error during upload." %}"></div>
 </div>
 <div class="fileinfo {% if widget.readonly and widget.has_tempfile_image %}thumbnail{% endif %}">
-- 
2.20.1

