在客户端用 JavaScript 访问 JPEGEXIF 旋转数据

我想旋转照片的基础上,他们的原始旋转,由设置在 JPEG EXIF 图像数据的相机。诀窍在于,所有这些都应该在浏览器中使用 JavaScript 和 <canvas>实现。

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

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

146122 次浏览

如果你想要它跨浏览器,你最好的选择是在服务器上进行。您可以拥有一个接收文件 URL 并返回 EXIF 数据的 API; PHP 有一个这样的模块

这可以通过使用 阿贾克斯来完成,因此对用户来说是无缝的。如果您不关心跨浏览器的兼容性,并且可以依赖于 HTML5文件功能,那么可以研究一下允许您用原生 JavaScript 获取数据的 JsJPEGmeta库。

可以将 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
});

Firefox 26支持 image-orientation: from-image: 根据 EXIF 数据,图像显示为纵向或横向

还有 一个在 Chrome 中实现这个的 bug

注意,这个属性只有 Firefox 支持,是 很可能被否定

Https://github.com/blueimp/javascript-load-image 是一个现代的 javascript 库,它不仅可以提取 exif 方向标志,还可以在客户端正确地镜像/旋转 JPEG 图像。

我刚刚用这个库解决了同样的问题: JS 客户端 Exif 定位: 旋转和镜像 JPEG 图像

如果你只想要方向标签而不想要其他东西,并且不想包含另外一个巨大的 javascript 库,我写了一个小代码来尽可能快地提取方向标签(它使用了在 IE10 + 中可用的 DataView 和 readAsArrayBuffer,但是你可以为老的浏览器编写你自己的数据阅读器) :

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

values:

-2: not jpeg
-1: not defined

enter image description here

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

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);
}

看看我写的一个模块(您可以在浏览器中使用它) ,它可以将 exif 方向转换为 CSS 转换: https://github.com/Sobesednik/exif2css

还有这个节点程序生成 JPEG 夹具的所有方向: https://github.com/Sobesednik/generate-exif-fixtures

我上传扩展代码 对于宽度大于高度的 img 标签,在 html 上以正常的方式显示机器人相机拍摄的照片,特别是对于宽度大于高度的 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]);


});

改进/添加更多功能到 Ali 之前的回答中,我在 Typecript 中创建了一个 util 方法,它适合我在这个问题上的需要。此版本以项目可能需要的度返回旋转。

ImageUtils.ts


/**
* Based on StackOverflow answer: https://stackoverflow.com/a/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 });
});
};