Commit 998b8f70 authored by Rich Barton's avatar Rich Barton Committed by Christopher Speller

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.
parent b03b9d73
......@@ -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) {
......
......@@ -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 = (
<img
className='file-preview__image'
src={getFileUrl(info.id)}
src={getFileThumbnailUrl(info.id)}
/>
);
} else {
......
......@@ -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 = (
<div
className='profile-img-preview'
style={{backgroundImage: 'url(' + this.state.image + ')'}}
style={imageStyles}
/>
);
} else {
......
......@@ -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",
......
......@@ -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"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment