From 998b8f70c2d88151b080657dea1ce0b9aca36d58 Mon Sep 17 00:00:00 2001 From: Rich Barton Date: Mon, 10 Jul 2017 06:51:07 -0700 Subject: PLT-6659 Fixed upload thumbnails that weren't properly rotated (#6816) - Used client-side EXIF data to rotate profile picture thumbnails - Added a small package for correctly translating EXIF orientation into CSS transforms - Instead of displaying the image using FileReader, used URL.createObjectURL because it is faster - For upload thumbnails, the original behavior was scaling the entire original image, without accounting for EXIF rotate. I changed this to use the thumbnail image, which does respect rotation. - The preview image was not available when the upload request returned, because handling the preview image creation was in a goroutine. I used sync.WaitGroup to block until the preview image creation is done. --- app/file.go | 25 ++++++++---- webapp/components/file_preview.jsx | 4 +- webapp/components/setting_picture.jsx | 72 +++++++++++++++++++++++++++++++++-- webapp/package.json | 1 + webapp/yarn.lock | 4 ++ 5 files changed, 92 insertions(+), 14 deletions(-) diff --git a/app/file.go b/app/file.go index d21fd4a14..a4e112e98 100644 --- a/app/file.go +++ b/app/file.go @@ -480,15 +480,24 @@ func DoUploadFile(teamId string, channelId string, userId string, rawFilename st } func HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) { - for i, data := range fileData { - go func(i int, data []byte) { - img, width, height := prepareImage(fileData[i]) - if img != nil { - go generateThumbnailImage(*img, thumbnailPathList[i], width, height) - go generatePreviewImage(*img, previewPathList[i], width) - } - }(i, data) + wg := new(sync.WaitGroup) + + for i := range fileData { + img, width, height := prepareImage(fileData[i]) + if img != nil { + wg.Add(2) + go func(img *image.Image, path string, width int, height int) { + defer wg.Done() + generateThumbnailImage(*img, path, width, height) + }(img,thumbnailPathList[i], width, height) + + go func(img *image.Image, path string, width int) { + defer wg.Done() + generatePreviewImage(*img, path, width) + }(img, previewPathList[i], width) + } } + wg.Wait() } func prepareImage(fileData []byte) (*image.Image, int, int) { diff --git a/webapp/components/file_preview.jsx b/webapp/components/file_preview.jsx index 65a71c047..0606c1b31 100644 --- a/webapp/components/file_preview.jsx +++ b/webapp/components/file_preview.jsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; -import {getFileUrl} from 'mattermost-redux/utils/file_utils'; +import {getFileThumbnailUrl} from 'mattermost-redux/utils/file_utils'; import PropTypes from 'prop-types'; @@ -39,7 +39,7 @@ export default class FilePreview extends React.Component { previewImage = ( ); } else { diff --git a/webapp/components/setting_picture.jsx b/webapp/components/setting_picture.jsx index faa463cc7..ec6dfbd20 100644 --- a/webapp/components/setting_picture.jsx +++ b/webapp/components/setting_picture.jsx @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import React, {Component} from 'react'; import {FormattedMessage} from 'react-intl'; +import exif2css from 'exif2css'; import FormError from 'components/form_error.jsx'; import loadingGif from 'images/load.gif'; @@ -41,26 +42,89 @@ export default class SettingPicture extends Component { } } + componentWillUnmount() { + if (this.previewBlob) { + URL.revokeObjectURL(this.previewBlob); + } + } + setPicture = (file) => { if (file) { - var reader = new FileReader(); + this.previewBlob = URL.createObjectURL(file); + var reader = new FileReader(); reader.onload = (e) => { + const orientation = this.getExifOrientation(e.target.result); + const orientationStyles = this.getOrientationStyles(orientation); + this.setState({ - image: e.target.result + image: this.previewBlob, + orientationStyles }); }; - reader.readAsDataURL(file); + reader.readAsArrayBuffer(file); + } + } + + // based on https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side/32490603#32490603 + getExifOrientation(data) { + var view = new DataView(data); + + if (view.getUint16(0, false) !== 0xFFD8) { + return -2; + } + + var length = view.byteLength; + var offset = 2; + + while (offset < length) { + var marker = view.getUint16(offset, false); + offset += 2; + + if (marker === 0xFFE1) { + if (view.getUint32(offset += 2, false) !== 0x45786966) { + return -1; + } + + var little = view.getUint16(offset += 6, false) === 0x4949; + offset += view.getUint32(offset + 4, little); + var tags = view.getUint16(offset, little); + offset += 2; + + for (var i = 0; i < tags; i++) { + if (view.getUint16(offset + (i * 12), little) === 0x0112) { + return view.getUint16(offset + (i * 12) + 8, little); + } + } + } else if ((marker & 0xFF00) === 0xFF00) { + offset += view.getUint16(offset, false); + } else { + break; + } } + return -1; + } + + getOrientationStyles(orientation) { + const { + transform, + 'transform-origin': transformOrigin + } = exif2css(orientation); + return {transform, transformOrigin}; } render() { let img; if (this.props.file) { + const imageStyles = { + backgroundImage: 'url(' + this.state.image + ')', + ...this.state.orientationStyles + }; + img = (
); } else { diff --git a/webapp/package.json b/webapp/package.json index ac9febbf3..7b17d0b1d 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -11,6 +11,7 @@ "bootstrap-colorpicker": "2.5.1", "chart.js": "2.5.0", "compass-mixins": "0.12.10", + "exif2css": "1.2.0", "fastclick": "1.0.6", "flux": "3.1.2", "font-awesome": "4.7.0", diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 66774deb5..ed9c41742 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2720,6 +2720,10 @@ executable@^1.0.0: dependencies: meow "^3.1.0" +exif2css@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/exif2css/-/exif2css-1.2.0.tgz#8438e116921508e3dcc30cbe2407b1d5535e1b45" + exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" -- cgit v1.2.3-1-g7c22