Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EXIF data (from mobile's cameras, in portrait mode) messing with image preview in Javascript

I started with this code which allows the user to preview the uploaded image in the browser:

'use strict';
var img = document.querySelector('img');
var span = document.querySelector('span');
document.querySelector('input').addEventListener('change', function(event){
  log('File changed', true);
  var file = event.target.files[0];
  if(file === undefined){
    img.parentElement.style.display = 'none';
    log('No file selected');
    return;
  }
  showImage(file);
});
function log(data, clear){
  if(clear){
    span.innerHTML = '';
  }
  span.innerHTML += '<br>' + data;
}
function showImage(file) {
  log('showImage()');
  if(!window.FileReader){
    return log('FileReader is not supported');
  }
  if(!window.FileReader.prototype.readAsDataURL){
    return log('readAsDataURL is not supported');
  }
  var reader = new FileReader();
  reader.onload = function(event){
    img.src = event.target.result;
    img.parentElement.style.display = 'block';
  };
  reader.readAsDataURL(file);
}
div{
  display:none;
}
img{
  display:block;
  height:100px;
  width:100px;
}
Picture: <input type="file">
<hr>
<div>
  Loaded:
  <img>
</div>
<hr>
Log...
<span></span>

Also available here: https://jsfiddle.net/grewt06v/

It was pretty straightforward and made the job really well. But then someone reported they had trouble previewing a picture taken in portrait mode using a Samsung device, it either rotated left or right. Here you have an example of both back and front camera pictures:

  • Samsung's back camera: Dropbox Google Drive
  • Samsung's front camera: Dropbox Google Drive

    Keep in mind the image content is altered to allow better understanding of the problem (the arrow should point always down), but the EXIF data remains the same. Also, when uploaded here, the EXIF data got stripped, so I had to use Google Drive Dropbox

So I had to make some changes in order to detect the EXIF and correctly rotate the image, which led me to find a way to check EXIF orientation and then to this code:

'use strict';
var original = document.querySelectorAll('img')[0];
var rotated = document.querySelectorAll('img')[1];
var span = document.querySelector('span');
document.querySelector('input').addEventListener('change', function(event){
  log('File changed', true);
  var file = event.target.files[0];
  if(file === undefined){
    original.parentElement.style.display = 'none';
    rotated.parentElement.style.display = 'none';
    log('No file selected');
    return;
  }
  getOrientation(file, showImage);
});
// Based on: https://stackoverflow.com/a/32490603/5503625
function getOrientation(file, callback) {
  log('getOrientation()');
  if(!window.FileReader){
    return log('FileReader is not supported');
  }
  if(!window.FileReader.prototype.readAsArrayBuffer){
    return log('readAsArrayBuffer is not supported');
  }
  var reader = new FileReader();
  reader.onload = function(e) {
    if(!window.DataView){
      return log('DataView is not supported');
    }
    if(!window.DataView.prototype.getUint16){
      return log('getUint16 is not supported');
    }
    if(!window.DataView.prototype.getUint32){
      return log('getUint32 is not supported');
    }
    var view = new DataView(e.target.result);
    if (view.getUint16(0, false) != 0xFFD8) return callback(file, -2);
    var length = view.byteLength, offset = 2;
    while (offset < length) {
      var marker = view.getUint16(offset, false);
      offset += 2;
      if (marker == 0xFFE1) {
        if (view.getUint32(offset += 2, false) != 0x45786966) return callback(file, -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 callback(file, view.getUint16(offset + (i * 12) + 8, little));
      }
      else if ((marker & 0xFF00) != 0xFF00) break;
      else offset += view.getUint16(offset, false);
    }
    return callback(file, -1);
  };
  reader.readAsArrayBuffer(file);
}
function log(data, clear){
  if(clear){
    span.innerHTML = '';
  }
  span.innerHTML += '<br>' + data;
}
function showImage(file, exifOrientation) {
  log('showImage()');
  log('EXIF orientation ' + exifOrientation);
  if(!window.FileReader){
    return log('FileReader is not supported');
  }
  if(!window.FileReader.prototype.readAsDataURL){
    return log('readAsDataURL is not supported');
  }
  var reader = new FileReader();
  reader.onload = function(event){
    original.src = event.target.result;
    rotated.src = event.target.result;
    original.parentElement.style.display = 'block';
    rotated.parentElement.style.display = 'block';
    var degrees = 0;
    switch(exifOrientation){
      case 1:
        // Normal
        break;
      case 2:
        // Horizontal flip
        break;
      case 3:
        // Rotated 180°
        degrees = 180;
        break;
      case 4:
        // Vertical flip
        break;
      case 5:
        // Rotated 90° -> Horizontal flip
        break;
      case 6:
        // Rotated 270°
        degrees = 90;
        break;
      case 7:
        // Rotated 90° -> Vertical flip
        break;
      case 8:
        // Rotated 90°
        degrees = 270;
        break;
    }
    var transform = 'rotate(' + degrees + 'deg)';
    log('transform:' + transform);
    rotated.style.transform = transform;
    rotated.style.webkitTransform = transform;
    rotated.style.msTransform = transform;
  };
  reader.readAsDataURL(file);
}
div{
  display:none;
}
img{
  display:block;
  height:100px;
  width:100px;
}
Picture: <input type="file">
<hr>
<div>
  Original
  <img>
</div>
<div>
  Rotated
  <img>
</div>
<hr>
Log...
<span></span>

Also available here: https://jsfiddle.net/grewt06v/1/

At that moment I thought I fixed the issue and it was all good again. But then someone reported having an issue they didn't have before, a trouble previewing pictures taken in portrait mote using an iPhone device, they are rotated to the right. Here you have an example of both back and front camera pictures:

  • iPhone's back camera: Dropbox Google Drive
  • iPhone's front camera: Dropbox Google Drive

    Again, keep in mind the image content is altered to allow better understanding of the problem (the arrow should point always down), but the EXIF data remains the same. Also, when uploaded here, the EXIF data got stripped, so I had to use Google Drive Dropbox

I'm not an image guru, so it took me a while to figure the problem was iPhone also stored EXIF rotation data (90 degrees clockwise), but the image content isn't rotated (I have no idea why they would do that, but I would like to know)

So, basically, the quickest, but probably not the best, solution was to go for browser detection using the navigator.userAgent to tell whether it was an iPhone so I wouldn't continue with the EXIF check

Can anyone come up with a better bullet-proof way to check for that (in case iPhone isn't the only one behaving like that)?

Update: Now that I checked the uploaded images, I found out Google Drive is having the same problem. Both Samsung's pictures are looking good and iPhone's pictures aren't. I feel kind of relieved, but I'd still like a better approach

Update: Google Drive removes EXIF information after using it to rotate the image, so I had to go for Dropbox. Thanks to @Kaiido for letting me know

like image 231
Piyin Avatar asked Sep 21 '17 22:09

Piyin


1 Answers

Like I said earlier, I'm not an image guru, so I think this answer still isn't the best one, but I came up with it and it suits my needs (hopefully it'll help someone else). Still, I'd like a really bullet-proof solution

So I thought about it and it actually only happens when the pictures are taken in portrait mode. So what I need to do is to check if the EXIF wants to rotate 90 or 270 degrees, and, if so, only perform the rotation if the height is less than the width (which would mean the portrait picture isn't already rotated). My code now looks like this:

'use strict';
var original = document.querySelectorAll('img')[0];
var rotated = document.querySelectorAll('img')[1];
var span = document.querySelector('span');
document.querySelector('input').addEventListener('change', function(event){
  log('File changed', true);
  var file = event.target.files[0];
  if(file === undefined){
    original.parentElement.style.display = 'none';
    rotated.parentElement.style.display = 'none';
    log('No file selected');
    return;
  }
  getOrientation(file, showImage);
});
// Based on: https://stackoverflow.com/a/32490603/5503625
function getOrientation(file, callback) {
  log('getOrientation()');
  if(!window.FileReader){
    return log('FileReader is not supported');
  }
  if(!window.FileReader.prototype.readAsArrayBuffer){
    return log('readAsArrayBuffer is not supported');
  }
  var reader = new FileReader();
  reader.onload = function(e) {
    if(!window.DataView){
      return log('DataView is not supported');
    }
    if(!window.DataView.prototype.getUint16){
      return log('getUint16 is not supported');
    }
    if(!window.DataView.prototype.getUint32){
      return log('getUint32 is not supported');
    }
    var view = new DataView(e.target.result);
    if (view.getUint16(0, false) != 0xFFD8) return callback(file, -2);
    var length = view.byteLength, offset = 2;
    while (offset < length) {
      var marker = view.getUint16(offset, false);
      offset += 2;
      if (marker == 0xFFE1) {
        if (view.getUint32(offset += 2, false) != 0x45786966) return callback(file, -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 callback(file, view.getUint16(offset + (i * 12) + 8, little));
      }
      else if ((marker & 0xFF00) != 0xFF00) break;
      else offset += view.getUint16(offset, false);
    }
    return callback(file, -1);
  };
  reader.readAsArrayBuffer(file);
}
function log(data, clear){
  if(clear){
    span.innerHTML = '';
  }
  span.innerHTML += '<br>' + data;
}
function showImage(file, exifOrientation) {
  log('showImage()');
  log('EXIF orientation ' + exifOrientation);
  if(!window.FileReader){
    return log('FileReader is not supported');
  }
  if(!window.FileReader.prototype.readAsDataURL){
    return log('readAsDataURL is not supported');
  }
  var reader = new FileReader();
  reader.onload = function(event){
    original.src = event.target.result;
    rotated.src = event.target.result;
    original.parentElement.style.display = 'block';
    rotated.parentElement.style.display = 'block';
    var degrees = 0;
    var portraitCheck = false;
    switch(exifOrientation){
      case 1:
        // Normal
        break;
      case 2:
        // Horizontal flip
        break;
      case 3:
        // Rotated 180°
        degrees = 180;
        break;
      case 4:
        // Vertical flip
        break;
      case 5:
        // Rotated 90° -> Horizontal flip
        break;
      case 6:
        // Rotated 270°
        degrees = 90;
        portraitCheck = true;
        break;
      case 7:
        // Rotated 90° -> Vertical flip
        break;
      case 8:
        // Rotated 90°
        degrees = 270;
        portraitCheck = true;
        break;
    }
    var img = document.createElement('img');
    img.style.visibility = 'none';
    document.body.appendChild(img);
    img.onload = function(){
      if(portraitCheck && this.height > this.width){
        log('Image already rotated');
        degrees = 0;
      }
      var transform = 'rotate(' + degrees + 'deg)';
      log('transform:' + transform);
      rotated.style.transform = transform;
      rotated.style.webkitTransform = transform;
      rotated.style.msTransform = transform;
      document.body.removeChild(this);
    }
    img.src = event.target.result;
  };
  reader.readAsDataURL(file);
}
div{
  display:none;
}
img{
  display:block;
  height:100px;
  width:100px;
}
Picture: <input type="file">
<hr>
<div>
  Original
  <img>
</div>
<div>
  Rotated
  <img>
</div>
<hr>
Log...
<span></span>

Also available here: https://jsfiddle.net/grewt06v/5/ https://jsfiddle.net/grewt06v/7/

The problem was also happening when saving the images on the server, and this solution is also working there

Update: It was working fine with my local tests, but not using Safari on the iPhone, so I had to load the image in the DOM first to get the accurate dimensions

like image 59
Piyin Avatar answered Nov 07 '22 00:11

Piyin