diff options
author | Matthew Somerville <matthew@mysociety.org> | 2020-06-30 16:46:29 +0100 |
---|---|---|
committer | Matthew Somerville <matthew@mysociety.org> | 2020-07-01 16:55:27 +0100 |
commit | 47df99daf6371eb3d7cdbe1ac9170a7f88749dc1 (patch) | |
tree | 4b4600c40ffffaea4f471457901965ad2b269aa3 /web/js/dropzone.js.patch | |
parent | e623594f10d74a8dfb5eaea801c43f4996b15a1e (diff) |
Fix photo orientation in modern browsers.
We use Dropzone (the photo upload library) to shrink photos client-side
before uploading in the background and display thumbnails. For the
resized upload, Dropzone restores the original Exif data, including
orientation, so it can be correctly oriented server-side; for a
thumbnail, it orients the image itself for immediate display.
Recently, browsers have started honouring Exif orientation much more
widely (Chrome 81+ and Firefox 77+ both now do it by default). This
means the data Dropzone gets from a resize has already been oriented
according to the Exif orientation data. Then Dropzone either looks at
the orientation to correct for display (thumbnail), or adds back the
Exif orientation data (upload) – in both cases, this leads to a double
implementation of the orientation, and an incorrect display.
To fix this, if we detect we are on a modern browser, we do not try and
fix orientation ourself [1], and in all cases we do not add any Exif
data back in (we only strip it server-side anyway). Conversely, that
means on a non-modern browser, we always perform a manual orientation
because no Exif data will be being sent server-side.
Also includes a fix to the orientation code [2] which wouldn't be
noticed in thumbnail generation as they are square, but could be now we
may be orienting full size photos.
[1] https://gitlab.com/meno/dropzone/-/merge_requests/80
[2] https://gitlab.com/meno/dropzone/-/merge_requests/45
Diffstat (limited to 'web/js/dropzone.js.patch')
-rw-r--r-- | web/js/dropzone.js.patch | 287 |
1 files changed, 248 insertions, 39 deletions
diff --git a/web/js/dropzone.js.patch b/web/js/dropzone.js.patch index b325b45d8..60a82709a 100644 --- a/web/js/dropzone.js.patch +++ b/web/js/dropzone.js.patch @@ -1,17 +1,92 @@ ---- dropzone.5.1.1.js 2017-06-30 09:46:43.000000000 +0100 -+++ dropzone.exiffixes.js 2017-06-30 18:25:27.000000000 +0100 -@@ -1175,9 +1175,7 @@ - }; - if ((typeof EXIF !== "undefined" && EXIF !== null) && fixOrientation) { - loadExif = function(callback) { +--- dropzone.5.1.1.js 2020-06-30 15:56:05.557790000 +0100 ++++ dropzone.exiffixes.js 2020-06-30 16:40:22.794951100 +0100 +@@ -26,7 +26,7 @@ + */ + + (function() { +- var Dropzone, Emitter, ExifRestore, camelize, contentLoaded, detectVerticalSquash, drawImageIOSFix, noop, without, ++ var Dropzone, Emitter, camelize, contentLoaded, detectVerticalSquash, drawImageIOSFix, noop, without, + slice = [].slice, + extend1 = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; +@@ -1123,7 +1123,7 @@ + }; + + Dropzone.prototype.resizeImage = function(file, width, height, resizeMethod, callback) { +- return this.createThumbnail(file, width, height, resizeMethod, false, (function(_this) { ++ return this.createThumbnail(file, width, height, resizeMethod, true, (function(_this) { + return function(dataUrl, canvas) { + var resizeMimeType, resizedDataURL; + if (canvas === null) { +@@ -1134,9 +1134,6 @@ + resizeMimeType = file.type; + } + resizedDataURL = canvas.toDataURL(resizeMimeType, _this.options.resizeQuality); +- if (resizeMimeType === 'image/jpeg' || resizeMimeType === 'image/jpg') { +- resizedDataURL = ExifRestore.restore(file.dataURL, resizedDataURL); +- } + return callback(Dropzone.dataURItoBlob(resizedDataURL)); + } + }; +@@ -1164,23 +1161,17 @@ + Dropzone.prototype.createThumbnailFromUrl = function(file, width, height, resizeMethod, fixOrientation, callback, crossOrigin) { + var img; + img = document.createElement("img"); ++ ++ // FixOrientation not needed anymore with browsers handling imageOrientation ++ fixOrientation = (getComputedStyle(document.body)['imageOrientation'] == 'from-image') ? false : fixOrientation; ++ + if (crossOrigin) { + img.crossOrigin = crossOrigin; + } + img.onload = (function(_this) { + return function() { +- var loadExif; +- loadExif = function(callback) { +- return callback(1); +- }; +- if ((typeof EXIF !== "undefined" && EXIF !== null) && fixOrientation) { +- loadExif = function(callback) { - return EXIF.getData(img, function() { - return callback(EXIF.getTag(this, 'Orientation')); - }); -+ return callback(EXIF.getData(img)); - }; - } - return loadExif(function(orientation) { -@@ -1601,7 +1599,7 @@ +- }; +- } +- return loadExif(function(orientation) { ++ var orientation = fixOrientation ? EXIF.getData(img) : 1; ++ + var canvas, ctx, ref, ref1, ref2, ref3, resizeInfo, thumbnail; + file.width = img.width; + file.height = img.height; +@@ -1212,23 +1203,23 @@ + break; + case 6: + ctx.rotate(0.5 * Math.PI); +- ctx.translate(0, -canvas.height); ++ ctx.translate(0, -canvas.width); + break; + case 7: + ctx.rotate(0.5 * Math.PI); +- ctx.translate(canvas.width, -canvas.height); ++ ctx.translate(canvas.height, -canvas.width); + ctx.scale(-1, 1); + break; + case 8: + ctx.rotate(-0.5 * Math.PI); +- ctx.translate(-canvas.width, 0); ++ ctx.translate(-canvas.height, 0); + } + drawImageIOSFix(ctx, img, (ref = resizeInfo.srcX) != null ? ref : 0, (ref1 = resizeInfo.srcY) != null ? ref1 : 0, resizeInfo.srcWidth, resizeInfo.srcHeight, (ref2 = resizeInfo.trgX) != null ? ref2 : 0, (ref3 = resizeInfo.trgY) != null ? ref3 : 0, resizeInfo.trgWidth, resizeInfo.trgHeight); + thumbnail = canvas.toDataURL("image/png"); + if (callback != null) { + return callback(thumbnail, canvas); + } +- }); ++ + }; + })(this); + if (callback != null) { +@@ -1601,7 +1592,7 @@ return results; }; @@ -20,31 +95,165 @@ Dropzone.isBrowserSupported = function() { var capableBrowser, j, len, ref, regex; -@@ -1904,6 +1902,27 @@ - var array, ato, buf, imageData, mae, separatePoint; - imageData = resizedFileBase64.replace('data:image/jpeg;base64,', ''); - buf = this.decode64(imageData); -+ -+ // Certain browsers (I'm looking at you, Safari) 'helpfully' provide their -+ // own EXIF data in the JPEG returned from HTMLCanvasElement.toDataURL. -+ // Dropzone doesn't take this into account when restoring the original -+ // file's EXIF, meaning the final uploaded file has two sets of EXIF. -+ // Certain JPEG tools (I'm looking at you, jhead) don't really handle this -+ // very well, either ignoring the duplicate EXIF, picking the wrong one -+ // or refusing to process the file entirely. -+ // Seems like the best way out of this mess is to make sure the uploaded -+ // JPEG only ever has one EXIF header. In this case, we want to keep the -+ // EXIF from the original file. -+ // This little loop inspects the new JPEG from the toDataURL call and -+ // strips out any existing EXIF headers (technically any APP1 headers, -+ // but same difference in this case). -+ for (var i = 0; i < buf.length; i++) { -+ if (buf[i] === 255 && buf[i+1] === 225) { -+ var length = buf[i + 2] * 256 + buf[i + 3] + 2; -+ buf.splice(i, length); -+ } -+ } -+ - separatePoint = buf.indexOf(255, 3); - mae = buf.slice(0, separatePoint); - ato = buf.slice(separatePoint); +@@ -1828,161 +1819,6 @@ + return ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio); + }; + +- ExifRestore = (function() { +- function ExifRestore() {} +- +- ExifRestore.KEY_STR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; +- +- ExifRestore.encode64 = function(input) { +- var chr1, chr2, chr3, enc1, enc2, enc3, enc4, i, output; +- output = ''; +- chr1 = void 0; +- chr2 = void 0; +- chr3 = ''; +- enc1 = void 0; +- enc2 = void 0; +- enc3 = void 0; +- enc4 = ''; +- i = 0; +- while (true) { +- 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 = ''; +- if (!(i < input.length)) { +- break; +- } +- } +- return output; +- }; +- +- ExifRestore.restore = function(origFileBase64, resizedFileBase64) { +- var image, rawImage, segments; +- if (!origFileBase64.match('data:image/jpeg;base64,')) { +- return resizedFileBase64; +- } +- rawImage = this.decode64(origFileBase64.replace('data:image/jpeg;base64,', '')); +- segments = this.slice2Segments(rawImage); +- image = this.exifManipulation(resizedFileBase64, segments); +- return 'data:image/jpeg;base64,' + this.encode64(image); +- }; +- +- ExifRestore.exifManipulation = function(resizedFileBase64, segments) { +- var aBuffer, exifArray, newImageArray; +- exifArray = this.getExifArray(segments); +- newImageArray = this.insertExif(resizedFileBase64, exifArray); +- aBuffer = new Uint8Array(newImageArray); +- return aBuffer; +- }; +- +- ExifRestore.getExifArray = function(segments) { +- var seg, x; +- seg = void 0; +- x = 0; +- while (x < segments.length) { +- seg = segments[x]; +- if (seg[0] === 255 & seg[1] === 225) { +- return seg; +- } +- x++; +- } +- return []; +- }; +- +- ExifRestore.insertExif = function(resizedFileBase64, exifArray) { +- var array, ato, buf, imageData, mae, separatePoint; +- 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; +- }; +- +- ExifRestore.slice2Segments = function(rawImageArray) { +- var endPoint, head, length, seg, segments; +- head = 0; +- segments = []; +- while (true) { +- if (rawImageArray[head] === 255 & rawImageArray[head + 1] === 218) { +- break; +- } +- if (rawImageArray[head] === 255 & rawImageArray[head + 1] === 216) { +- head += 2; +- } else { +- 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; +- }; +- +- ExifRestore.decode64 = function(input) { +- var base64test, buf, chr1, chr2, chr3, enc1, enc2, enc3, enc4, i, output; +- output = ''; +- chr1 = void 0; +- chr2 = void 0; +- chr3 = ''; +- enc1 = void 0; +- enc2 = void 0; +- enc3 = void 0; +- enc4 = ''; +- i = 0; +- buf = []; +- base64test = /[^A-Za-z0-9\+\/\=]/g; +- if (base64test.exec(input)) { +- console.warning('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, ''); +- while (true) { +- 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 = ''; +- if (!(i < input.length)) { +- break; +- } +- } +- return buf; +- }; +- +- return ExifRestore; +- +- })(); +- +- + /* + * contentloaded.js + * |