移动设备相机的EXIF数据(纵向模式)对Javascript中图像预览造成干扰

7

我从这段代码开始,它允许用户在浏览器中预览上传的图像

'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>

此处也可访问:https://jsfiddle.net/grewt06v/

这段代码一开始很简单易懂且表现良好。但随后有人报告称,在使用三星设备拍摄竖屏照片预览时遇到了问题,照片会被错误地左右旋转。以下是前后置摄像头拍摄的例子:

  • 三星后置摄像头:Dropbox Google Drive
  • 三星前置摄像头:Dropbox Google Drive

    请记住,图像内容已经修改以便更好地理解问题(箭头总应指向下),但EXIF数据保持不变。此外,当在此处上载时,EXIF数据已被删除,因此我必须使用Google Drive Dropbox来处理。

为了检测EXIF并正确旋转图像,我不得不进行一些更改,这也让我找到了检查EXIF方向的方法,最终导致了以下代码:

'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://dev59.com/NGsz5IYBdhLWcg3w5MI0#32490603
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>

此处还可找到:https://jsfiddle.net/grewt06v/1/

那时我以为问题已经解决了,一切都好了。但后来有人报告说他们之前没有遇到的问题,即使用iPhone设备预览以纵向模式拍摄的照片时,它们会被旋转到右侧。以下是前后摄像头照片的示例:

  • iPhone 后置摄像头:Dropbox Google Drive
  • iPhone 前置摄像头:Dropbox Google Drive

    请注意,图像内容已更改以便更好地理解问题(箭头应始终指向下方),但 EXIF 数据保持不变。此外,上传到这里时,EXIF 数据被删除了,所以我必须使用 Google Drive Dropbox。

我不是图像专家,所以花了一段时间才发现问题在于 iPhone 也存储了 EXIF 旋转数据(顺时针旋转90度),但图像内容并没有旋转(我不知道他们为什么要这样做,但我想知道)

因此,最快但可能不是最好的解决方案是使用 navigator.userAgent 进行浏览器检测,以判断是否为 iPhone,这样我就不会继续进行 EXIF 检查了。

有人能想出更好的、可靠的方法来检测这一点吗(万一 iPhone 不是唯一表现出这种行为的设备)?

更新:现在我检查了上传的图片,发现 Google Drive 也有同样的问题。三星的照片看起来很好,而 iPhone 的照片则不好。我感到有些宽慰,但我仍然希望有更好的方法。

更新:Google Drive 在使用 EXIF 信息旋转图像后会删除它,因此我必须使用 Dropbox。感谢 @Kaiido 让我知道


1
你的 iPhone 图片似乎没有任何 EXIF 数据,请再检查一下? - Kaiido
你说得对,我没有仔细检查,显然谷歌云盘使用EXIF数据来正确渲染图像,然后丢弃它(三星的EXIF数据也被丢弃了)。我使用Dropbox更新了链接。 - Piyin
你需要一种可靠的方法来检测 iphoneexif 旋转吗?如果是后者,你可以尝试这个:https://github.com/exif-js/exif-js - aletzo
并不是上述所说的任何一种情况,更像是一种检测图像内容是否已经旋转的防弹方法,即使图像具有EXIF数据(iPhone的奇怪方式),我也不会再次旋转它。我尝试了这个库,但它做的和我一样,所以iPhone的图像仍然基于EXIF旋转,链接在这里:https://jsfiddle.net/grewt06v/2/ - Piyin
1个回答

1

就像我之前所说,我不是一位图像专家,所以我认为这个答案还不是最好的,但我想出了它并且适合我的需求(希望它能帮助其他人)。然而,我想要一个真正强大的解决方案。

所以我想了想,实际上只有在照片以纵向模式拍摄时才会发生这种情况。因此,我需要检查EXIF是否要旋转90度或270度,并且如果高度小于宽度(这意味着肖像照片尚未旋转),则仅执行旋转。我的代码现在看起来像这样:

'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://dev59.com/NGsz5IYBdhLWcg3w5MI0#32490603
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>

这里也有可用的版本:https://jsfiddle.net/grewt06v/5/ https://jsfiddle.net/grewt06v/7/

当将图片保存在服务器上时,该问题也会发生,此解决方案在那里也有效。

更新:在我的本地测试中一切正常,但在iPhone上使用Safari时不行,因此我必须先在DOM中加载图像以获得准确的尺寸。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接