在客户端JavaScript中访问JPEG EXIF旋转数据

141

我想根据JPEG EXIF图像数据中相机设置的原始旋转来旋转照片,而且所有这些都应该在浏览器中使用JavaScript和<canvas>完成。

JavaScript如何访问JPEG、本地文件API对象、本地<img>或远程<img>、EXIF数据以读取旋转信息?

服务器端的答案不可行,我正在寻找客户端解决方案。

8个回答

291

如果你只需要方向标签而不想包含其他的巨大JavaScript库,我写了一段简短的代码,尽可能快地提取方向标签(它使用DataView和readAsArrayBuffer,在IE10+中可用,但你可以为旧浏览器编写自己的数据读取器):

function getOrientation(file, callback) {
    var reader = new FileReader();
    reader.onload = function(e) {

        var view = new DataView(e.target.result);
        if (view.getUint16(0, false) != 0xFFD8)
        {
            return callback(-2);
        }
        var length = view.byteLength, offset = 2;
        while (offset < length) 
        {
            if (view.getUint16(offset+2, false) <= 8) return callback(-1);
            var marker = view.getUint16(offset, false);
            offset += 2;
            if (marker == 0xFFE1) 
            {
                if (view.getUint32(offset += 2, false) != 0x45786966) 
                {
                    return callback(-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(view.getUint16(offset + (i * 12) + 8, little));
                    }
                }
            }
            else if ((marker & 0xFF00) != 0xFF00)
            {
                break;
            }
            else
            { 
                offset += view.getUint16(offset, false);
            }
        }
        return callback(-1);
    };
    reader.readAsArrayBuffer(file);
}

// usage:
var input = document.getElementById('input');
input.onchange = function(e) {
    getOrientation(input.files[0], function(orientation) {
        alert('orientation: ' + orientation);
    });
}
<input id='input' type='file' />

值:

-2: not jpeg
-1: not defined

这里输入图片描述

对于使用Typescript的用户,您可以使用以下代码:

export const getOrientation = (file: File, callback: Function) => {
  var reader = new FileReader();

  reader.onload = (event: ProgressEvent) => {

    if (! event.target) {
      return;
    }

    const file = event.target as FileReader;
    const view = new DataView(file.result as ArrayBuffer);

    if (view.getUint16(0, false) != 0xFFD8) {
        return callback(-2);
    }

    const length = view.byteLength
    let offset = 2;

    while (offset < length)
    {
        if (view.getUint16(offset+2, false) <= 8) return callback(-1);
        let marker = view.getUint16(offset, false);
        offset += 2;

        if (marker == 0xFFE1) {
          if (view.getUint32(offset += 2, false) != 0x45786966) {
            return callback(-1);
          }

          let little = view.getUint16(offset += 6, false) == 0x4949;
          offset += view.getUint32(offset + 4, little);
          let tags = view.getUint16(offset, little);
          offset += 2;
          for (let i = 0; i < tags; i++) {
            if (view.getUint16(offset + (i * 12), little) == 0x0112) {
              return callback(view.getUint16(offset + (i * 12) + 8, little));
            }
          }
        } else if ((marker & 0xFF00) != 0xFF00) {
            break;
        }
        else {
            offset += view.getUint16(offset, false);
        }
    }
    return callback(-1);
  };

  reader.readAsArrayBuffer(file);
}

3
PNG或GIF没有标准格式来存储图像方向。https://dev59.com/KWkw5IYBdhLWcg3w8fFJ - Ali
2
对我来说工作正常,但我需要更改最后一行为reader.readAsArrayBuffer(file);而不是slice,因为我打算将缓冲区用于我的base64图像,否则,您只会看到图像的第一个切片。顺便说一句,如果您只需要方向信息,则不需要此操作。谢谢。 - Philip Murphy
2
@DaraJava 我移除了“切片”部分,因为有时标签会在限制后出现,但如果永远找不到标签,它将减慢操作。无论如何,与方向标记不同,Flash标记不在IFD0目录中,而我的代码只搜索此部分。要获取Flash标记,必须搜索SubIFD目录。您可以在这里找到关于EXIF的好教程:https://www.media.mit.edu/pia/Research/deepview/exif.html - Ali
@Ali:在reader.readAsArrayBuffer(file);中读取整个文件是至关重要的吗?还是我们可以只读取较少的字节?我之所以问这个问题,是因为我希望解决方案不受用户选择的文件大小的影响。毕竟,我们只需要方向标签,没有其他东西。 - Hassan Baig
@HassanBaig,不需要了,之前我使用过slice,但是由于标题更大所以我去掉了它。 - Ali
显示剩余20条评论

24
你可以使用exif-js库与HTML5文件API结合使用:http://jsfiddle.net/xQnMd/1/
$("input").change(function() {
    var file = this.files[0];  // file
        fr   = new FileReader; // to read file contents

    fr.onloadend = function() {
        // get EXIF data
        var exif = EXIF.readFromBinaryFile(new BinaryFile(this.result));

        // alert a value
        alert(exif.Make);
    };

    fr.readAsBinaryString(file); // read the file
});

尝试将binaryajax.js包含在我的项目中,会导致访问被拒绝的错误。 - Obi Wan
EXIF对象从哪里来?BinaryFile脚本似乎没有包含它,据我所知,它也不是jQuery或我经常使用的任何其他脚本的一部分... - jrista
@jrista 你需要添加2个JS文件。查看答案中的jsfiddle链接:http://www.nihilogic.dk/labs/exif/exif.js + http://www.nihilogic.dk/labs/binaryajax/binaryajax.js - DemiImp
6
图书馆网站似乎无法访问,我发现的其他ExifReader库在浏览器支持方面都有限制。 有什么好的替代品吗? - Praxis Ashelin
Binaryajax.js 可以在这里找到,https://github.com/jseidelin/binaryajax/blob/master/binaryajax.js - Peppelorum
显示剩余3条评论

20

5
谢谢提供错误报告的链接。我已经为之加星,这样Chrome团队就知道更多人需要此功能。 - DemiImp
根据Chromium项目成员在https://bugs.chromium.org/p/chromium/issues/detail?id=158753#c104中的评论:“更改将在Chrome 81中进行。这将在8-10周时间内作为稳定版本向公众推出。” - jeff forest
1
从81版本开始在Chrome上实现。不过,人们更新他们的浏览器需要一些时间-请注意[caniuse](https://caniuse.com/#feat=css-image-orientation) - Robin Métral

11

6
我上传了扩展代码,可以在某些img标签上正常显示由安卓相机拍摄的照片,并进行正确旋转,特别是对于宽度大于高度的img标签。我知道这段代码很丑,但您不需要安装任何其他软件包。(我使用以上代码来获取exif旋转值,谢谢。)
function getOrientation(file, callback) {
  var reader = new FileReader();
  reader.onload = function(e) {

    var view = new DataView(e.target.result);
    if (view.getUint16(0, false) != 0xFFD8) return callback(-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(-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(view.getUint16(offset + (i * 12) + 8, little));
      }
      else if ((marker & 0xFF00) != 0xFF00) break;
      else offset += view.getUint16(offset, false);
    }
    return callback(-1);
  };
  reader.readAsArrayBuffer(file);
}

var isChanged = false;
function rotate(elem, orientation) {
    if (isIPhone()) return;

    var degree = 0;
    switch (orientation) {
        case 1:
            degree = 0;
            break;
        case 2:
            degree = 0;
            break;
        case 3:
            degree = 180;
            break;
        case 4:
            degree = 180;
            break;
        case 5:
            degree = 90;
            break;
        case 6:
            degree = 90;
            break;
        case 7:
            degree = 270;
            break;
        case 8:
            degree = 270;
            break;
    }
    $(elem).css('transform', 'rotate('+ degree +'deg)')
    if(degree == 90 || degree == 270) {
        if (!isChanged) {
            changeWidthAndHeight(elem)
            isChanged = true
        }
    } else if ($(elem).css('height') > $(elem).css('width')) {
        if (!isChanged) {
            changeWidthAndHeightWithOutMargin(elem)
            isChanged = true
        } else if(degree == 180 || degree == 0) {
            changeWidthAndHeightWithOutMargin(elem)
            if (!isChanged)
                isChanged = true
            else
                isChanged = false
        }
    }
}


function changeWidthAndHeight(elem){
    var e = $(elem)
    var width = e.css('width')
    var height = e.css('height')
    e.css('width', height)
    e.css('height', width)
    e.css('margin-top', ((getPxInt(height) - getPxInt(width))/2).toString() + 'px')
    e.css('margin-left', ((getPxInt(width) - getPxInt(height))/2).toString() + 'px')
}

function changeWidthAndHeightWithOutMargin(elem){
    var e = $(elem)
    var width = e.css('width')
    var height = e.css('height')
    e.css('width', height)
    e.css('height', width)
    e.css('margin-top', '0')
    e.css('margin-left', '0')
}

function getPxInt(pxValue) {
    return parseInt(pxValue.trim("px"))
}

function isIPhone(){
    return (
        (navigator.platform.indexOf("iPhone") != -1) ||
        (navigator.platform.indexOf("iPod") != -1)
    );
}

然后使用类似于的东西。
$("#banner-img").change(function () {
    var reader = new FileReader();
    getOrientation(this.files[0], function(orientation) {
        rotate($('#banner-img-preview'), orientation, 1)
    });

    reader.onload = function (e) {
        $('#banner-img-preview').attr('src', e.target.result)
        $('#banner-img-preview').css('display', 'inherit')

    };

    // read the image file as a data URL.
    reader.readAsDataURL(this.files[0]);

});

5

在Ali之前的回答的基础上,我改进并添加了更多功能,创建了一个适合我需要的Typescript实用程序方法来解决这个问题。这个版本返回旋转角度,你可能也需要在你的项目中使用。

ImageUtils.ts

/**
 * Based on StackOverflow answer: https://dev59.com/NGsz5IYBdhLWcg3w5MI0#32490603
 *
 * @param imageFile The image file to inspect
 * @param onRotationFound callback when the rotation is discovered. Will return 0 if if it fails, otherwise 0, 90, 180, or 270
 */
export function getOrientation(imageFile: File, onRotationFound: (rotationInDegrees: number) => void) {
  const reader = new FileReader();
  reader.onload = (event: ProgressEvent) => {
    if (!event.target) {
      return;
    }

    const innerFile = event.target as FileReader;
    const view = new DataView(innerFile.result as ArrayBuffer);

    if (view.getUint16(0, false) !== 0xffd8) {
      return onRotationFound(convertRotationToDegrees(-2));
    }

    const length = view.byteLength;
    let offset = 2;

    while (offset < length) {
      if (view.getUint16(offset + 2, false) <= 8) {
        return onRotationFound(convertRotationToDegrees(-1));
      }
      const marker = view.getUint16(offset, false);
      offset += 2;

      if (marker === 0xffe1) {
        if (view.getUint32((offset += 2), false) !== 0x45786966) {
          return onRotationFound(convertRotationToDegrees(-1));
        }

        const little = view.getUint16((offset += 6), false) === 0x4949;
        offset += view.getUint32(offset + 4, little);
        const tags = view.getUint16(offset, little);
        offset += 2;
        for (let i = 0; i < tags; i++) {
          if (view.getUint16(offset + i * 12, little) === 0x0112) {
            return onRotationFound(convertRotationToDegrees(view.getUint16(offset + i * 12 + 8, little)));
          }
        }
        // tslint:disable-next-line:no-bitwise
      } else if ((marker & 0xff00) !== 0xff00) {
        break;
      } else {
        offset += view.getUint16(offset, false);
      }
    }
    return onRotationFound(convertRotationToDegrees(-1));
  };
  reader.readAsArrayBuffer(imageFile);
}

/**
 * Based off snippet here: https://github.com/mosch/react-avatar-editor/issues/123#issuecomment-354896008
 * @param rotation converts the int into a degrees rotation.
 */
function convertRotationToDegrees(rotation: number): number {
  let rotationInDegrees = 0;
  switch (rotation) {
    case 8:
      rotationInDegrees = 270;
      break;
    case 6:
      rotationInDegrees = 90;
      break;
    case 3:
      rotationInDegrees = 180;
      break;
    default:
      rotationInDegrees = 0;
  }
  return rotationInDegrees;
}

使用方法:

import { getOrientation } from './ImageUtils';
...
onDrop = (pics: any) => {
  getOrientation(pics[0], rotationInDegrees => {
    this.setState({ image: pics[0], rotate: rotationInDegrees });
  });
};

嗨,这很有帮助,谢谢。但是当我从安卓手机拍照时,它给我的方向是6,而当我从iPhone拍照时,它给我的方向是3,如何更改这个设置,使我的照片方向不旋转,并保持与拍摄时相同的方向。请帮帮我。基本上,由于方向6是90度(如您的代码中所述),我想做的就是将其变为0度,但我不知道如何做到这一点。 - user17534350

4
如果你想实现跨浏览器功能,最好在服务器上完成。你可以有一个API,它接受文件URL并返回EXIF数据; PHP有一个模块可以做到这一点
这可以使用Ajax来完成,因此对用户来说是无缝的。如果您不关心跨浏览器兼容性,并且可以依赖于HTML5文件功能,请查看库JsJPEGmeta,它将允许您在本机JavaScript中获取该数据。

21
你需要明白,Stack Overflow为每个人回答问题,而不仅仅是最初提出问题的人。下一个有与你相同目标的人可能是一名PHP开发者——为什么你要否定他们可以获得Xeon06提供的信息?仅因为不想要PHP解决方案而将其编辑掉是不恰当的。 - Jon Skeet
7
问题中提到了“在JavaScript中”,因此该部分与问题无关。网站上已经有许多其他类似的PHP问题和答案,这对于这个问题来说是不必要的噪音。 - Mikko Ohtamaa
2
如果有人要求JavaScript解决方案,他们不希望看到PHP解决方案作为第一篇帖子。 - Mikko Ohtamaa
1
@MikkoOhtamaa,似乎大多数人不同意你的看法 http://meta.stackexchange.com/questions/157338/answer-edited-to-be-less-complete-because-of-language-bias你似乎对回答你问题的权利有一些错误的所有感。 - Alex Turpin
1
我编辑了答案,将正确答案放在开头。抱歉造成困扰。 - Mikko Ohtamaa
显示剩余7条评论

3

1
不错的模块!但是,它首先如何从JPEG中获取EXIF信息? - Mikko Ohtamaa
@MikkoOhtamaa 谢谢,但是不行,你必须使用 exif-js 或 exiftool 服务器端来完成。 - zavr
这很有用。但是在我看来,它似乎只能正确处理竖向照片,而不能处理横向的照片。 - Sridhar Sarnobat

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